Programming

basic programming

Infinispan Deployment.YAML on OpenShift Container Platform

Deploying Infinispan to OpenShift can be done easily by using either Operator or Helm chart. However, there is an even easier way to deploy it, and that is by using a single YAML file.

---
kind: Deployment
apiVersion: apps/v1
metadata:
  name: infinispan
  namespace: cache
  labels:
    app: infinispan
spec:
  replicas: 1
  selector:
    matchLabels:
      app: infinispan
  template:
    metadata:
      labels:
        app: infinispan
    spec:
      volumes:
        - name: data-volume
          persistentVolumeClaim:
            claimName: ispn-pv
      containers:
        - name: infinispan
          image: 'quay.io/infinispan/server:16.1'
          imagePullPolicy: IfNotPresent
          resources:
            limits:
              memory: 1Gi
            requests:
              memory: 1Gi
          env:
            - name: USER
              value: admin
            - name: PASS
              value: password
          ports:
            - name: infinispan
              containerPort: 11222
              protocol: TCP
          volumeMounts:
            - name: data-volume
              mountPath: /opt/infinispan/server/data
---
kind: Service
apiVersion: v1
metadata:
  name: infinispan
  namespace: cache
spec:
  ports:
    - protocol: TCP
      port: 11222
      targetPort: 11222
  selector:
    app: infinispan
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: ispn-pv
  namespace: cache
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  volumeMode: Filesystem

We can deploy this single file directly to OpenShift and have it up and running in a few minutes. However this approach is perfect for local development, testing, or a quick proof-of-concept. If you intend to scale Infinispan into a multi-node distributed cluster for production, you should migrate this configuration to a StatefulSet or leverage the official Infinispan Operator to handle cluster discovery and data replication.

Creating a Service Account to Access OpenShift Container Registry

Let’s say you want to create an OpenShift Container Registry account to be used by your CI/CD tooling. The recommended approach is to use a ServiceAccount instead of a regular user account. Here’s how you can do it.

First, create a ServiceAccount,

$ oc create serviceaccount david-susugigi-sa

Next, generate a token for this ServiceAccount. In this example, we create a long-lived token with a lifespan of two years

$ oc create token david-susugigi-sa --duration=16760h

eyJhbGciOiJ.....Gog8tY

Then, assign the appropriate role to the ServiceAccount

$ oc policy add-role-to-user system:image-builder -z david-susugigi-sa

Finally, use the ServiceAccount to log in to the registry, using the token as the password

$ podman login default-route-openshift-image-registry.apps-crc.testing \
      --tls-verify=false \ 
      -u david-susugigi-sa \ 
      -p eyJhbGciOiJ.....Gog8tY

Login Succeeded!

[Spring Boot] Implementing Multiple Cache Managers

Typically, we leverage a single cache for a specific purpose. however, there are scenarios where we need to integrate multiple caching mechanisms, each serving a different role. In this tutorial, I will demonstrate how to configure multiple Cache Managers in Spring Boot and illustrate the integration between the application layer, database, local cache, and remote cache.

In this scenario, we will use both a local cache (Caffeine) and a remote cache (Infinispan) to store query results.

First we’ll start with a simple pom.xml which contains Spring Boot, MySQL, Infinispan server, and Caffeine for local caches.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.edw</groupId>
    <artifactId>spring-boot-multiple-cacheable</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.9</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.infinispan</groupId>
                <artifactId>infinispan-bom</artifactId>
                <version>16.0.5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <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>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- caches -->
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-spring-boot3-starter-remote</artifactId>
        </dependency>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-jboss-marshalling</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <layout>JAR</layout>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

its configuration on application.properties

### server
server.port=8080
spring.application.name=spring-boot-multiple-cacheable

## log
logging.level.root=INFO
logging.level.com.edw=DEBUG

spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql=TRACE

## db
spring.datasource.url=jdbc:mysql://localhost:3306/test_db
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=password

## infinispan
jdg.servers=localhost:11222
jdg.userName=admin2
jdg.password=password

## caffeine
caffeine.spec=maximumSize=500,expireAfterAccess=10m

And some Java files,

package com.edw;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}
package com.edw.model;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.infinispan.api.annotations.indexing.Indexed;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor

@Indexed

@Entity
@Table(name = "t_employee")
public class Employee implements Serializable {
    @Id
    public Long id;

    @Column(name = "gender")
    public String gender;

    @Column(name = "firstname")
    public String firstname;

    @Column(name = "lastname")
    public String lastname;
}
package com.edw.repository;

import com.edw.model.Employee;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
    List<Employee> findTop10ByFirstnameLikeAndLastnameLikeIgnoreCase(String firstname, String lastname);
}
package com.edw.service;

import com.edw.model.Employee;
import com.edw.repository.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class EmployeeService {

    private final EmployeeRepository employeeRepository;

    public EmployeeService(@Autowired EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    @Cacheable(value = "query-cache",
            key = "#id",
            cacheManager = "infinispanCacheManager")
    public Employee getEmployee(Long id) {
        return employeeRepository.findById(id).orElse(new Employee());
    }

    @Cacheable(value = "employee-cache",
            key = "#root.methodName + '-' + #firstname + '-' +  #lastname",
            cacheManager = "caffeineCacheManager")
    public List<Employee> findEmployeesByFirstnameAndLastname(String firstname, String lastname) {
        return employeeRepository.findTop10ByFirstnameLikeAndLastnameLikeIgnoreCase(firstname + "%", lastname + "%");
    }
}
package com.edw.controller;

import com.edw.model.Employee;
import com.edw.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Slf4j
@RestController
public class EmployeeController {

    private final EmployeeService employeeService;

    public EmployeeController(@Autowired EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    @GetMapping("/employee/{id}")
    public ResponseEntity getEmployeeById (@PathVariable Long id) {
        log.info("getEmployeeById");

        Employee employee = employeeService.getEmployee(id);
        if(employee.getId() == null)
            return ResponseEntity.notFound().build();

        return ResponseEntity.ok(employee);
    }

    @GetMapping("/employee/find-by-firstname-and-lastname/{firstname}/{lastname}")
    public ResponseEntity getEmployeeByFirstnameAndLastname (@PathVariable String firstname, @PathVariable String lastname) {
        log.info("getEmployeeByFirstnameAndLastname");

        List<Employee> employees = employeeService.findEmployeesByFirstnameAndLastname(firstname, lastname);
        if(employees.isEmpty())
            return ResponseEntity.notFound().build();

        return ResponseEntity.ok(employees);
    }

}

And this is the most important part, where we configure our Cache connections. We use @Primary on the Infinispan manager to set it as the default, but we use explicit bean names (caffeineCacheManager and infinispanCacheManager) to distinguish them in the service layer.

package com.edw.config;

import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.configuration.ClientIntelligence;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
import org.infinispan.jboss.marshalling.commons.GenericJBossMarshaller;
import org.infinispan.spring.remote.provider.SpringRemoteCacheManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class InfinispanConfiguration {
    @Value("${jdg.servers}")
    private String jdgHostAddress;

    @Value("${jdg.userName}")
    private String userName;

    @Value("${jdg.password}")
    private String password;

    @Primary
    @Bean(name="infinispanCacheManager")
    public CacheManager infinispanCacheManager() {
        return new SpringRemoteCacheManager(remoteCacheManager());
    }

    @Bean
    public RemoteCacheManager remoteCacheManager() {
        return new RemoteCacheManager(
                new ConfigurationBuilder()
                        .addServers(jdgHostAddress)
                        .security().authentication().username(userName).password(password)
                        .clientIntelligence(ClientIntelligence.HASH_DISTRIBUTION_AWARE)
                        .marshaller(new GenericJBossMarshaller())
                        .build());
    }
}
package com.edw.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CaffeineConfiguration {

    @Value("${caffeine.spec}")
    private String spec;

    @Bean(name = "caffeineCacheManager")
    public CacheManager caffeineCacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager("employee-cache");
        manager.setCaffeine(Caffeine.from(spec));
        return manager;
    }

}

We can try to trigger API call and can see that the first hit will go to the database, but after that it will always go to Cache to reduce the database workload.

$ curl -kv http://localhost:8080/employee/1

Code for this project can be accessed in the below repository,

https://github.com/edwin/spring-boot-multiple-cacheable

Increasing Resource for Red Hat Migration Toolkit for Applications

I am currently working on a project that involves migrating a legacy Java web application from Java 8 to Java 21. Fortunately, Red Hat offers a specialized tool for this exact use case: the Red Hat Migration Toolkit for Applications (MTA). It can be installed as a CLI on a VM or as an Operator on OpenShift.

For this specific project, I am leveraging Red Hat MTA on OpenShift via the Operator. While it works seamlessly for most scenarios, I encountered an interesting issue where the analysis process would hang and eventually throw an error.

The root cause is that MTA “Tackle” tasks have default resource limits. These limits can prevent the tool from processing large .WAR files efficiently. When the file is too large for the allocated overhead, the process simply stalls.

The workaround is straightforward: we can increase the MTA resource limits by modifying the Tackle YAML file. In the example below, I have adjusted the configuration to a 4 CPU limit and an 8 GB RAM limit to provide enough headroom for heavy analysis.

apiVersion: tackle.konveyor.io/v1alpha1
kind: Tackle
metadata:
  name: tackle
  namespace: openshift-mta
spec:
  analyzer_container_requests_cpu: 4
  analyzer_container_requests_memory: 8Gi
  feature_auth_required: 'true'
  provider_java_container_limits_cpu: 4
  provider_java_container_requests_memory: 8Gi
  analyzer_container_limits_memory: 8Gi
  analyzer_container_limits_cpu: 4
  provider_java_container_requests_cpu: 4
  provider_java_container_limits_memory: 8Gi