[Canonical page](<https://docs.dhtmlx.com/planning-engine/java/how-to/call-from-async-code/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Call from async code

Run a long scheduling call from a Spring or Jakarta EE handler, a reactive pipeline or a UI event without blocking the calling thread.

Every [`Project`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md>) method is synchronous and CPU-bound. There's no `CompletableFuture` variant and no cancellation parameter. Move the heavy calls ([`level`](<https://docs.dhtmlx.com/planning-engine/java/api/project-level.md>), [`evaluate`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md>), [`suggestAssignments`](<https://docs.dhtmlx.com/planning-engine/java/api/project-suggest-assignments.md>) on large projects) to an executor with `CompletableFuture.supplyAsync`.

## Prerequisites

- A solved [`Project`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md>): [`solve(data, options)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) has run.
- Only one thread uses that `Project` at a time. A `Project` isn't synchronized. If several requests reach the same instance, see [Share a project between threads](<https://docs.dhtmlx.com/planning-engine/java/how-to/share-a-project-between-threads.md>).
- An `ExecutorService` for engine work. A fixed pool of platform threads, sized to your cores, fits CPU-bound calls.

## Steps

1. Copy the project on the thread that owns it. `copy()` updates sharing flags on the original too, so don't call it from the background thread. The background call works on the copy, and the original stays usable while the call runs. [`copy()`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md#copy>) is cheap, because copies share their data until one of them is edited.

   ```java
   Project draft = project.copy();
   ```

2. Run the call on the executor:

   ```java
   CompletableFuture<LevelingResult> pending =
       CompletableFuture.supplyAsync(() -> draft.level(new LevelingOptions()), engine);
   ```

   Exceptions such as [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>) and [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>) complete the future exceptionally. `join()` throws them wrapped in a `CompletionException`, and `get()` wraps them in an `ExecutionException`. Read the original from `getCause()`.

3. To stop waiting after a timeout, add `orTimeout`:

   ```java
   CompletableFuture<LevelingResult> pending = CompletableFuture
       .supplyAsync(() -> draft.level(new LevelingOptions()), engine)
       .orTimeout(30, java.util.concurrent.TimeUnit.SECONDS);
   ```

   The timeout can't stop a call that has already started. The future completes with a `TimeoutException` and returns control to you, but the call keeps running on the executor thread until it finishes. `cancel(true)` on a `CompletableFuture` doesn't interrupt that thread either. That's why the call runs on `draft`. When you stop waiting, drop the copy. The original stays untouched.

4. Commit the result. [`level`](<https://docs.dhtmlx.com/planning-engine/java/api/project-level.md>) has already accepted the leveled state on `draft`, so replace the original with it:

   ```java
   project = draft;
   ```

   Replace the original only if nobody edited it while the call ran. Otherwise the result comes from an older state, so run the call again on a new copy.

   [`evaluate`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md>) and [`suggestAssignments`](<https://docs.dhtmlx.com/planning-engine/java/api/project-suggest-assignments.md>) never modify the project, so there's nothing to swap. Pass the change set you pick to [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>) on the original.

The full example levels two tasks in the background and gives up after 30 seconds:

```java
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
import org.dhtmlx.plen.*;

public class CallFromAsyncCode {
  static TaskData task(String id, Moment start, double duration) {
    return new TaskData(Id.str(id)).withStartDate(start).withDuration(duration);
  }

  static AssignmentData booking(String task, String resource) {
    return new AssignmentData()
        .withTask(Id.str(task))
        .withResource(Id.str(resource))
        .withValue(1.0);
  }

  public static void main(String[] args) throws Exception {
    // Monday, August 10, 2026, on the default calendar.
    var monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
    var data = new ProjectData()
        .withTasks(List.of(task("A", monday, 2.0), task("B", monday, 2.0)))
        .withResources(List.of(new ResourceData().withId(Id.str("dev1"))))
        .withAssignments(List.of(booking("A", "dev1"), booking("B", "dev1")));

    var project = new Project();
    project.solve(data, new SolveOptions());

    Project draft = project.copy();
    try (ExecutorService engine = Executors.newFixedThreadPool(2)) {
      CompletableFuture<LevelingResult> pending = CompletableFuture
          .supplyAsync(() -> draft.level(new LevelingOptions()), engine)
          .orTimeout(30, java.util.concurrent.TimeUnit.SECONDS);
      try {
        LevelingResult leveled = pending.join();
        project = draft;
        for (Mutation update : leveled.schedule.updates) {
          if (update.startDate != null && update.endDate != null) {
            System.out.println(update.id + ": " + update.startDate + " .. " + update.endDate
                + ", delay " + update.levelingDelay);
          }
        }
      } catch (CompletionException e) {
        if (!(e.getCause() instanceof TimeoutException)) throw e;
        System.out.println("leveling timed out; the project is unchanged");
      }
    }
  }
}
```

`org.dhtmlx.plen` has its own [`TimeUnit`](<https://docs.dhtmlx.com/planning-engine/java/api/data/time-unit.md>) enum, so the example imports the `java.util.concurrent` classes by name and spells out `java.util.concurrent.TimeUnit`. Closing the `ExecutorService` at the end of the `try` block waits for running tasks. In a server, keep one long-lived executor instead of one per request.

## Score candidates on several cores

[`evaluateWith`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md#evaluatewith>) with [`Execution.parallel(...)`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/execution.md>) spreads candidates over a thread pool of its own, but it still blocks the caller until every candidate is scored. Run it through `supplyAsync` too:

```java
Project copy = project.copy();
CompletableFuture<EvaluateResult> scores = CompletableFuture.supplyAsync(
    () -> copy.evaluateWith(candidates, new EvaluateOptions(), Execution.parallel(2)),
    engine);
```

`Execution.parallel()` uses `Runtime.getRuntime().availableProcessors()` threads. On a shared server, pass a smaller count, such as `Execution.parallel(2)`, so one request doesn't take every core.

If the thread blocked in `evaluateWith` is interrupted, the call throws an `IllegalStateException` that wraps the `InterruptedException`, and it sets the thread's interrupt flag again. Candidates already handed to the pool still run to the end on its threads. No other call reacts to interrupts.

## Virtual threads

Virtual threads (`Executors.newVirtualThreadPerTaskExecutor()`) suit the side that waits: a request handler can block on `join()` without holding a platform thread. The engine call itself never blocks on I/O, so on a virtual thread it occupies a carrier thread for its whole run. Keep the engine work on a bounded platform pool and let the virtual thread wait for its future.

## Work that outlives the request

`join()` still keeps the request open until the call returns. For a call that takes longer than a client should wait, submit it to a background worker, return right away, and let the client fetch the result later. Follow the same rules in the worker: give it its own [`copy()`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md#copy>), made on the thread that owns the project.

## Result

The example prints the task that leveling moved. The calling thread only waits in `join()`, while [`level`](<https://docs.dhtmlx.com/planning-engine/java/api/project-level.md>) runs on the executor:

```text
B: 2026-08-12 08:00 .. 2026-08-14 08:00, delay 2.0
```

If leveling takes longer than 30 seconds, the example prints `leveling timed out; the project is unchanged`, and `project` keeps its unleveled baseline. For the leveling flow itself, see [Preview leveling](<https://docs.dhtmlx.com/planning-engine/java/how-to/preview-leveling.md>). For the exceptions each call throws, see [Error handling](<https://docs.dhtmlx.com/planning-engine/java/error-handling.md>).
