1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.model.constraint;
7   
8   import org.eclipse.jdt.annotation.Owning;
9   
10  import java.util.Objects;
11  import java.util.concurrent.ExecutorService;
12  import java.util.concurrent.ForkJoinPool;
13  import java.util.concurrent.TimeUnit;
14  
15  import edu.umd.cs.findbugs.annotations.NonNull;
16  import edu.umd.cs.findbugs.annotations.Nullable;
17  
18  /**
19   * Configuration for parallel constraint validation.
20   * <p>
21   * This class supports two modes:
22   * <ul>
23   * <li>Internal thread pool: Created via {@link #withThreads(int)}, shut down by
24   * {@link #close()}. Uses {@link ForkJoinPool} internally to handle nested
25   * parallelism without deadlock.</li>
26   * <li>External executor: Provided via {@link #withExecutor(ExecutorService)},
27   * NOT shut down by {@link #close()}</li>
28   * </ul>
29   * <p>
30   * Instances should be used with try-with-resources or explicitly closed after
31   * validation.
32   */
33  public final class ParallelValidationConfig implements AutoCloseable {
34  
35    /**
36     * Single-threaded sequential execution (default, current behavior).
37     * <p>
38     * This instance does not need to be closed.
39     */
40    @SuppressWarnings("resource")
41    @NonNull
42    public static final ParallelValidationConfig SEQUENTIAL = new ParallelValidationConfig(null, 1, false);
43  
44    /**
45     * The executor service, lazily initialized if using internal pool.
46     * <p>
47     * Volatile is required for thread-safe lazy initialization.
48     */
49    @SuppressWarnings("PMD.AvoidUsingVolatile") // Required for thread-safe lazy initialization
50    @Nullable
51    private volatile ExecutorService executor;
52    private final int threadCount;
53    private final boolean ownsExecutor;
54  
55    private ParallelValidationConfig(@Nullable ExecutorService executor, int threadCount, boolean ownsExecutor) {
56      this.executor = executor;
57      this.threadCount = threadCount;
58      this.ownsExecutor = ownsExecutor;
59    }
60  
61    /**
62     * Create configuration using an application-provided executor.
63     * <p>
64     * The executor is NOT shut down by {@link #close()}; the caller retains
65     * ownership.
66     * <p>
67     * The caller owns the returned configuration and is responsible for closing it.
68     *
69     * @param executor
70     *          the executor service to use for parallel tasks
71     * @return configuration using the provided executor
72     * @throws NullPointerException
73     *           if executor is null
74     */
75    @NonNull
76    @Owning
77    public static ParallelValidationConfig withExecutor(@NonNull ExecutorService executor) {
78      Objects.requireNonNull(executor, "executor must not be null");
79      return new ParallelValidationConfig(executor, 0, false);
80    }
81  
82    /**
83     * Create configuration that creates an internal thread pool.
84     * <p>
85     * The internal pool is shut down when {@link #close()} is called.
86     * <p>
87     * The caller owns the returned configuration and is responsible for closing it.
88     *
89     * @param threadCount
90     *          number of threads (must be &gt;= 1)
91     * @return configuration with internal thread pool
92     * @throws IllegalArgumentException
93     *           if threadCount &lt; 1
94     */
95    @NonNull
96    @Owning
97    public static ParallelValidationConfig withThreads(int threadCount) {
98      if (threadCount < 1) {
99        throw new IllegalArgumentException("threadCount must be at least 1, got: " + threadCount);
100     }
101     if (threadCount == 1) {
102       return SEQUENTIAL;
103     }
104     return new ParallelValidationConfig(null, threadCount, true);
105   }
106 
107   /**
108    * Check if parallel execution is enabled.
109    *
110    * @return true if using more than one thread
111    */
112   public boolean isParallel() {
113     return executor != null || threadCount > 1;
114   }
115 
116   /**
117    * Get the executor service, creating an internal pool if needed.
118    * <p>
119    * For internal pools, the executor is created lazily on first call.
120    *
121    * @return the executor service
122    * @throws IllegalStateException
123    *           if called on SEQUENTIAL config
124    */
125   @NonNull
126   public ExecutorService getExecutor() {
127     if (!isParallel()) {
128       throw new IllegalStateException("Cannot get executor for sequential configuration");
129     }
130     ExecutorService result = executor;
131     if (result == null) {
132       synchronized (this) {
133         result = executor;
134         if (result == null) {
135           // Use ForkJoinPool to avoid deadlock with nested parallelism.
136           // Fixed thread pools deadlock when all threads wait for children.
137           result = new ForkJoinPool(threadCount);
138           executor = result;
139         }
140       }
141     }
142     return Objects.requireNonNull(result, "Executor should not be null after initialization");
143   }
144 
145   /**
146    * Shut down internal executor if one was created.
147    * <p>
148    * Does nothing if using an external executor or if no executor was created.
149    */
150   @Override
151   public void close() {
152     ExecutorService exec = executor;
153     if (ownsExecutor && exec != null) {
154       exec.shutdown();
155       try {
156         if (!exec.awaitTermination(60, TimeUnit.SECONDS)) {
157           exec.shutdownNow();
158         }
159       } catch (@SuppressWarnings("unused") InterruptedException ex) {
160         exec.shutdownNow();
161         Thread.currentThread().interrupt();
162       }
163     }
164   }
165 }