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 >= 1)
91 * @return configuration with internal thread pool
92 * @throws IllegalArgumentException
93 * if threadCount < 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 }