DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Managed Scheduled Executor Service vs EJB Timer
  • Deep Dive Into Java Executor Framework
  • Mastering Concurrency: An In-Depth Guide to Java's ExecutorService
  • The Challenges and Pitfalls of Using Executors in Java

Trending

  • Unlocking the Potential of Apache Iceberg: A Comprehensive Analysis
  • 5 Subtle Indicators Your Development Environment Is Under Siege
  • A Developer's Guide to Mastering Agentic AI: From Theory to Practice
  • Measuring the Impact of AI on Software Engineering Productivity
  1. DZone
  2. Coding
  3. Java
  4. Efficient Task Management: Building a Java-Based Task Executor Service for Your Admin Panel

Efficient Task Management: Building a Java-Based Task Executor Service for Your Admin Panel

Implementation of a simple yet effective Java-based task executor service, analysis advantages, and finding simples solution.

By 
Timur Mukhitdinov user avatar
Timur Mukhitdinov
·
May. 10, 23 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
5.4K Views

Join the DZone community and get the full member experience.

Join For Free

In today's data-driven world, effectively managing and processing large volumes of data is crucial for organizations to maintain a competitive edge. As systems become more complex and interconnected, developers must grapple with an array of challenges, such as data migrations, data backfilling, generating analytical reports, etc. 

In this article, we will explore a simple yet effective Java-based implementation of a task executor service. We will discuss the key components of the service, examine some advantages of this approach, and provide insights to help developers make informed decisions when implementing a task executor service for their own projects.

System Overview

Service consists of a cluster of N servers responsible for processing user requests (API Servers), along with an administrator panel that allows monitoring and managing data within the system (Admin Panel). 

Key features:

  • API Servers and Admin Panel use a shared database to ensure efficient data access and storage. 
  • Access to the Admin Panel is restricted and available only to authorized personnel. 
  • All actions performed within the Admin Panel are logged in persistent storage for the following audit

Use Case: Display Available Tasks and Execute Them From the UI

In this use case, the Admin Panel's UI displays a list of available tasks and their status (either executing or idle). The task names are human-readable and uniquely identify the specific process they correspond to. Operators can trigger a task by its name, and the task's status will update in the UI.

Regarding the use case, we can determine the following functional requirements for server-side implementation:

  1. Obtaining a list of task names and statuses.
  2. Trigger task execution by its name
  3. The status of the task is set to “EXECUTING” when execution starts and to “IDLE” after it’s done.
  4. Parallel execution of the same task is not allowed due to it being idle or running at the same time.

Technical Implementation

Task Interface

To begin the implementation process, we define the task interface. Each task should have a human-readable name and a method to initiate its execution. This interface sets the foundation for custom tasks implemented to specific use cases.

Java
 
public interface Task {

   /**
    * Returns the human-readable name of the task. Must be unique across existing tasks
    *
    * @return the name of the task
    */
   String getName();

   /**
    * Starts the execution of the task.
    */
   void execute();
}


By implementing this interface, developers can create custom execution logic with distinctive and descriptive names, ensuring that the task is easily identifiable within the system.

Implementation Covers Functional Requirements

Considering that the Admin Panel is a single-instance service and all the tasks are executed on it, we can store task statuses in memory, allowing for straightforward tracking of task invocations and start/finish events. This design choice simplifies the task management process and reduces the overhead associated with querying external data sources to fetch task statuses.

Therefore, the next step can be covering the functional requirements:

Java
 
public class TaskService {

   private final Map<String, Task> tasks;
   private final Map<String, Boolean> running = new HashMap<>();

   public TaskService(List<Task> tasks) {
       this.tasks = tasks.stream().collect(Collectors.toMap(Task::getName, Function.identity()));
       this.tasks.keySet().forEach(taskName -> running.put(taskName, false));
   }

   public Map<String, Boolean> getTasksStatuses() {
       return Collections.unmodifiableMap(running);
   }

   public void start(String name) {
       Task task = tasks.get(name);
       if (task == null) {
           throw new IllegalArgumentException("Unknown task '" + name + '\'');
       }
       if (running.get(name)) {
           throw new IllegalStateException("Task is executing");
       }
       running.put(name, true);
       task.execute();
       running.put(name, false);
   }
}


Method getTaskStatuses provides good enough encapsulation: it returns an unmodifiable wrapper of the internal state: if the value is true — the task is executing; otherwise — idle. The method is refined. There is nothing to improve here. 

We've implicitly validated that there are no duplicate task names. In the constructor, we stream the tasks and collect them into the map with a default merging strategy — throwing an exception on a duplicate key.

Method start(String) checks if the task exists, then changes the task status, executes the task, and sets the status back. At first glance, it seems that it works fine, but this method has some issues.

  1. Execution may throw an exception, so the state of the task will remain unchanged after failed execution.
  2. Concurrency issue with test-and-set.
  3. The task executes in the same thread, which means that, in our case, we use the HTTP request thread. We should respond right after triggering the invocation, so the client application can easily determine that the server received the request and started working on it.
  4. Auditing logs are required — after some time, you may need to connect the dots and check when a task was executed. At that moment, server logs could be unreachable. Therefore, you need to write events of the invocation to a database.

Improving the Implementation

Keep in mind that the invocation may be interrupted, so we must always maintain the state. This particular case is quite obvious: we can easily wrap the invocation into a try block and change the status back in the final block.

Java
 
try {
   ...
} finally {
   running.put(name, false);
}


To fix the concurrency issue, we need to understand how happens before semantics works. running.get and running.put together are not atomic operations and therefore are not thread-safe. If there are two concurrent threads, both can pass testing the running.get(name) before  running.put and nothing will prevent both of them from triggering the invocation. 

There are several ways to solve this issue. It could be a synchronized block, a lock, atomics (make it Map<String, AtomicBoolean> running). I'll use ConcurrentHashMap instead.

Interface Map provides operations compute, computeIfAbsent and computeIfPresent – they’re atomic in ConcurrentHashMap, which means that whatever lambda you provide will be thread safe because it will be executed sequentially.

Java
 
running.compute(name, (key, isRunning) -> {
   if (Boolean.TRUE.equals(isRunning)) {
       throw new TaskLockUnavailable(name + " is already running");
   }
   return true;
});
// here is safe to invoke the task


To release the triggering thread, we need to pass execution to java.util.concurrent.ExecutorService. Validation should be synchronous. It creates a positive user experience when the user sees validation errors without delay. So, we need to check if the task is not found by the name and or is already executing before submitting the task to the executor.

Summarizing all the thoughts into a code listing, below is the finalized start method:

Java
 
public void start(String name) {
   Task task = tasks.get(name);
   if (task == null) {
       throw new IllegalArgumentException("Unknown task '" + name + '\'');
   }
   running.compute(name, (key, isRunning) -> {
       if (Boolean.TRUE.equals(isRunning)) {
           throw new TaskLockUnavailable(name + " is already running");
       }
       return true;
   });
   executorService.submit(() -> {
       try {
           doExecuteTask(task);
       } finally {
           running.put(name, false);
       }
   });
}


As you can see, I’ve defined doExecuteTask as a separate method. I did it to enhance the invocation by saving events for the following audit. It may look like this: 

Java
 
private void doExecuteTask(Task task) {
   String name = task.getName();
   try {
       taskExecutionAuditor.started(name);
       task.execute();
       taskExecutionAuditor.done(name);
   } catch (Exception e) {
       taskExecutionAuditor.error(name, e);
   }
}


Implementation of the TaskExecutionAuditor I’ll leave it out of the scope of this article, as it usually heavily depends on specific requirements, which could vary greatly between different use cases and organizations.

Conclusion

In this article, we dove into the problem of invoking long-running tasks in a service with multiple API Servers and an Admin Panel. We examined a simple yet effective Java-based task executor service tailored for an Admin Panel, which despite its simplicity, efficiently manages and processes large volumes of data.

While we covered the core aspects of the task-executing service, there are several topics beyond the scope of this article. These topics include interrupting task invocations, restoring and continuing running tasks after the server restarts, scaling the invocation, and more. These advanced features may be worthwhile to consider when designing a robust and scalable task management solution.

Nevertheless, the presented implementation serves as a solid foundation for a sort of MVP of your own task invocation framework. It provides a starting point from which you can further develop and customize the solution to meet your specific functional and non-functional requirements. By developing upon this foundation, you can create a powerful task management system that caters to the unique needs of your application and infrastructure. This will ensure efficient and reliable processing of long-running tasks.

Executor (software) Java (programming language) Task (computing)

Opinions expressed by DZone contributors are their own.

Related

  • Managed Scheduled Executor Service vs EJB Timer
  • Deep Dive Into Java Executor Framework
  • Mastering Concurrency: An In-Depth Guide to Java's ExecutorService
  • The Challenges and Pitfalls of Using Executors in Java

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!