Lejdi Prifti

0 %
Lejdi Prifti
Software Engineer
DevOps Engineer
ML Practitioner
  • Residence:
    Albania
  • City:
    Tirana
  • Email:
    info@lejdiprifti.com
Spring
AWS & Cloud
Angular
Team Player
Coordination & Leadership
Time Management
Docker & Kubernetes
ReactJs
JavaScript
Python
  • Java, JavaScript, Python
  • AWS, Kubernetes, Azure
  • Bootstrap, Materialize
  • Css, Sass, Less
  • Blockchain, Ethereum, Solidity
  • React, React Native, Flutter
  • GIT knowledge
  • Machine Learning, Deep Learning
0

No products in the basket.

Building a Timer Using ScheduledExecutorService in Java

21. October 2024

In this article, we will explore how to implement a timer mechanism using the ScheduledExecutorService, an important tool in Java for scheduling tasks to run after a given delay or periodically.

We’ll look at how this service simplifies managing timed tasks, offering more flexibility and control compared to traditional Timer classes such as Timer and TimerTask .

Table of Contents

What is a Timer?

timer is a mechanism or tool used to measure time intervals and execute specific actions after a certain period or at regular intervals. Timers are commonly used in both hardware and software applications to automate tasks or trigger events based on time.

Types of Timers:

  1. One-Time Timers: These timers execute a task after a single delay. For example, saving the data in the database after 5 seconds from their creation.
  2. Recurring Timers: These timers execute a task repeatedly at regular intervals. For example, sending a reminder email every day at noon.

Use Case: In-memory Data Storage with Timed Saves

Consider a use case where an application needs to collect data in-memory (e.g., sensor data) and periodically store it in the database to optimize performance.

If you’re building an application that processes real-time data, making individual database round trips for each data point in a single transaction can result in overwhelming the database and increased latency.

ScheduledExecutorService can be used to schedule regular saves of the accumulated data at fixed intervals, ensuring that data is reliably persisted without overwhelming the database with frequent writes.

Application setup

I’m setting up a simple Spring Boot application with WebSocket support, Spring Data JPA, and an embedded H2 database.

I’m creating a simple entity called SensorDataEntity to store the data received from a “live” sensor. This entity will be managed using Spring Data JPA, and the data will be saved in an embedded H2 database.

				
					@Entity
@Table(name = "sensor_data")
@Data
public class SensorDataEntity implements Serializable {

   private static final long serialVersionUID = 8839199832305252792L;
  
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;
   
   private String message;
  
   @CreationTimestamp
   private Timestamp createdAt;
   
   public SensorDataEntity(String message) {
    this.message = message;
   }

}
				
			

Next, I created a repository called SensorDataRepository to handle database operations for SensorDataEntity. Additionally, I implemented a service that will manage the buffering mechanism before saving the sensor data.

				
					@Slf4j
@Service
@RequiredArgsConstructor
public class SensorDataServiceImpl implements SensorDataService {

     private final List<SensorDataEntity> SENSOR_DATA_LIST = Collections.synchronizedList(new ArrayList<>());
    
     private final SensorDataRepository repository;
    
     private ScheduledExecutorService scheduler;
    
     private ScheduledFuture<?> currentTimer;
    
     @PostConstruct
     public void init() {
      scheduler = Executors.newScheduledThreadPool(1);
     }
    
     @Override
     public void buffer(SensorDataEntity entity) {
      if (SENSOR_DATA_LIST.size() + 1 > 5) {
       this.repository.saveAllAndFlush(SENSOR_DATA_LIST);
       SENSOR_DATA_LIST.clear();
      }
      SENSOR_DATA_LIST.add(entity);
      this.resetTimer();
     }
    
     private void resetTimer() {
      if (currentTimer != null) {
       currentTimer.cancel(false);
      }
      currentTimer = scheduler.schedule(() -> {
       log.info("saving sensor in db");
       List<SensorDataEntity> currentList = new ArrayList<>(SENSOR_DATA_LIST);
       SENSOR_DATA_LIST.clear();
       repository.saveAllAndFlush(currentList);
      }, 5000, TimeUnit.MILLISECONDS);
     }
}
				
			

After the service is created and all dependencies are injected, the init() method is called to initialize the executorService with a single thread. The executor is used to manage the timing of saving sensor data.

The buffer method buffers incoming sensor data by adding the entity to SENSOR_DATA_LIST. It also calls resetTimer(), which resets the timer to save the buffered data after a delay.

How the buffering works

  • Immediate save when buffer is full:

If more than 5 sensor data entries are buffered, the entire list is saved to the database and cleared. This reduces the risk of holding too much data in memory.

  • Efficiency:

Data is saved in batches when the buffer reaches the limit (5 entries), making the process more efficient by reducing the number of database calls.

  • Fallback Timer:

If the buffer doesn’t fill up to 5 entries, the existing resetTimer() logic ensures that the data will still be saved after the specified delay (e.g., 5 seconds).

WebSocket Message Handler

The code below defines a ServerWebSocketHandler that manages WebSocket connections. For more information, refer to this article.

In the message handler, for every message received, I construct a SensorDataEntity and call the SensorDataService to handle it.

				
					@RequiredArgsConstructor
public class ServerWebSocketHandler implements WebSocketHandler {

 private static final Logger LOGGER = LoggerFactory.getLogger(ServerWebSocketHandler.class);

 private final SensorDataService sensorDataService;

 @Override
 public void afterConnectionEstablished(WebSocketSession session) throws Exception {
  LOGGER.info("connection opened in session with id {}", session.getId());
 }

 @Override
 public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
  LOGGER.info("message arrived");
  sensorDataService.buffer(new SensorDataEntity(TextMessage.class.cast(message).getPayload()));
 }

 @Override
 public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
  LOGGER.error("error in session with id {}", session.getId());
 }

 @Override
 public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
  LOGGER.info("connection closed in session with id {}", session.getId());
 }

 @Override
 public boolean supportsPartialMessages() {
  return false;
 }

}
				
			

Conclusion

Now, it is time to test it with Postman.

As we can see, for every message I send, the message gets buffered. After 5 seconds, the timer is triggered to save the data in the database.

Key Points:

  • Thread safetySENSOR_DATA_LIST is a synchronized list, ensuring that multiple threads can safely modify it.
  • Efficient saving: Instead of saving each data point as it arrives, the buffering mechanism groups data and saves it all at once, reducing the number of database writes.
  • Timer resetting: The use of a scheduled task with resetTimer() allows the service to adjust dynamically based on incoming data, making the saving process more efficient.

Note: This scenario is straightforward. The primary goal of this article is to explain the buffering mechanism and demonstrate how to create a one-time timer using ScheduledExecutorService .

For more articles on Spring Boot, check out the blog.

Posted in SpringBootTags:
Write a comment