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.List;
11 import java.util.Objects;
12 import java.util.concurrent.ExecutorService;
13 import java.util.concurrent.ForkJoinPool;
14 import java.util.concurrent.TimeUnit;
15
16 import edu.umd.cs.findbugs.annotations.NonNull;
17 import edu.umd.cs.findbugs.annotations.Nullable;
18
19 /**
20 * Configuration for constraint validation.
21 * <p>
22 * This class supports parallel execution and optional event-based
23 * instrumentation. Two execution modes are available:
24 * <ul>
25 * <li>Internal thread pool: Created via {@link #withThreads(int)}, shut down by
26 * {@link #close()}. Uses {@link ForkJoinPool} internally to handle nested
27 * parallelism without deadlock.</li>
28 * <li>External executor: Provided via {@link #withExecutor(ExecutorService)},
29 * NOT shut down by {@link #close()}</li>
30 * </ul>
31 * <p>
32 * An optional {@link ValidationEventListener} can be configured via
33 * {@link #withListener(ValidationEventListener)} to receive callbacks during
34 * validation. By default, a {@link NoOpValidationEventListener} is used,
35 * ensuring zero overhead when instrumentation is not needed.
36 * <p>
37 * Instances should be used with try-with-resources or explicitly closed after
38 * validation.
39 */
40 public final class ValidationConfig implements AutoCloseable {
41
42 /**
43 * Single-threaded sequential execution (default, current behavior).
44 * <p>
45 * This instance does not need to be closed.
46 */
47 @SuppressWarnings("resource")
48 @NonNull
49 public static final ValidationConfig SEQUENTIAL = new ValidationConfig(null, 1, false,
50 NoOpValidationEventListener.INSTANCE);
51
52 /**
53 * The executor service, lazily initialized if using internal pool.
54 * <p>
55 * Volatile is required for thread-safe lazy initialization.
56 */
57 @SuppressWarnings("PMD.AvoidUsingVolatile") // Required for thread-safe lazy initialization
58 @Nullable
59 private volatile ExecutorService executor;
60 private final int threadCount;
61 private final boolean ownsExecutor;
62 @NonNull
63 private final ValidationEventListener listener;
64
65 private ValidationConfig(
66 @Nullable ExecutorService executor,
67 int threadCount,
68 boolean ownsExecutor,
69 @NonNull ValidationEventListener listener) {
70 this.executor = executor;
71 this.threadCount = threadCount;
72 this.ownsExecutor = ownsExecutor;
73 this.listener = listener;
74 }
75
76 /**
77 * Create configuration using an application-provided executor.
78 * <p>
79 * The executor is NOT shut down by {@link #close()}; the caller retains
80 * ownership.
81 * <p>
82 * The caller owns the returned configuration and is responsible for closing it.
83 *
84 * @param executor
85 * the executor service to use for parallel tasks
86 * @return configuration using the provided executor
87 * @throws NullPointerException
88 * if executor is null
89 */
90 @NonNull
91 @Owning
92 public static ValidationConfig withExecutor(@NonNull ExecutorService executor) {
93 Objects.requireNonNull(executor, "executor must not be null");
94 return new ValidationConfig(executor, 0, false, NoOpValidationEventListener.INSTANCE);
95 }
96
97 /**
98 * Create configuration that creates an internal thread pool.
99 * <p>
100 * The internal pool is shut down when {@link #close()} is called.
101 * <p>
102 * The caller owns the returned configuration and is responsible for closing it.
103 *
104 * @param threadCount
105 * number of threads (must be >= 1)
106 * @return configuration with internal thread pool
107 * @throws IllegalArgumentException
108 * if threadCount < 1
109 */
110 @NonNull
111 @Owning
112 public static ValidationConfig withThreads(int threadCount) {
113 if (threadCount < 1) {
114 throw new IllegalArgumentException("threadCount must be at least 1, got: " + threadCount);
115 }
116 if (threadCount == 1) {
117 return SEQUENTIAL;
118 }
119 return new ValidationConfig(null, threadCount, true, NoOpValidationEventListener.INSTANCE);
120 }
121
122 /**
123 * Create a new configuration with the specified event listener, preserving the
124 * current parallel execution settings.
125 * <p>
126 * If this config owns its executor, the derived config will lazily create its
127 * own independent pool to avoid unsafe sharing of owned executors.
128 *
129 * @param listener
130 * the event listener to use for validation instrumentation
131 * @return a new configuration with the given listener
132 * @throws NullPointerException
133 * if listener is null
134 */
135 @NonNull
136 @Owning
137 public ValidationConfig withListener(@NonNull ValidationEventListener listener) {
138 Objects.requireNonNull(listener, "listener must not be null");
139 if (this.ownsExecutor) {
140 // Don't share owned executor; derived config will lazily create its own
141 return new ValidationConfig(null, this.threadCount, true, listener);
142 }
143 return new ValidationConfig(this.executor, this.threadCount, false, listener);
144 }
145
146 /**
147 * Create a new configuration that adds an additional event listener, preserving
148 * the current parallel execution settings and any existing listener.
149 * <p>
150 * If the current listener is a {@link NoOpValidationEventListener}, the new
151 * listener replaces it directly. Otherwise, a
152 * {@link CompositeValidationEventListener} is created to deliver events to both
153 * the existing and new listeners.
154 *
155 * @param additionalListener
156 * the additional event listener to add
157 * @return a new configuration with the additional listener
158 * @throws NullPointerException
159 * if additionalListener is null
160 */
161 @NonNull
162 @Owning
163 public ValidationConfig addListener(@NonNull ValidationEventListener additionalListener) {
164 Objects.requireNonNull(additionalListener, "additionalListener must not be null");
165 ValidationEventListener current = this.listener;
166 if (current instanceof NoOpValidationEventListener) {
167 return withListener(additionalListener);
168 }
169 return withListener(
170 new CompositeValidationEventListener(List.of(current, additionalListener)));
171 }
172
173 /**
174 * Check if parallel execution is enabled.
175 *
176 * @return true if using more than one thread
177 */
178 public boolean isParallel() {
179 return executor != null || threadCount > 1;
180 }
181
182 /**
183 * Get the configured validation event listener.
184 *
185 * @return the event listener, never null
186 */
187 @NonNull
188 public ValidationEventListener getListener() {
189 return listener;
190 }
191
192 /**
193 * Get the executor service, creating an internal pool if needed.
194 * <p>
195 * For internal pools, the executor is created lazily on first call.
196 *
197 * @return the executor service
198 * @throws IllegalStateException
199 * if called on SEQUENTIAL config
200 */
201 @NonNull
202 public ExecutorService getExecutor() {
203 if (!isParallel()) {
204 throw new IllegalStateException("Cannot get executor for sequential configuration");
205 }
206 ExecutorService result = executor;
207 if (result == null) {
208 synchronized (this) {
209 result = executor;
210 if (result == null) {
211 // Use ForkJoinPool to avoid deadlock with nested parallelism.
212 // Fixed thread pools deadlock when all threads wait for children.
213 result = new ForkJoinPool(threadCount);
214 executor = result;
215 }
216 }
217 }
218 return Objects.requireNonNull(result, "Executor should not be null after initialization");
219 }
220
221 /**
222 * Shut down internal executor if one was created.
223 * <p>
224 * Does nothing if using an external executor or if no executor was created.
225 */
226 @Override
227 public void close() {
228 ExecutorService exec = executor;
229 if (ownsExecutor && exec != null) {
230 exec.shutdown();
231 try {
232 if (!exec.awaitTermination(60, TimeUnit.SECONDS)) {
233 exec.shutdownNow();
234 }
235 } catch (@SuppressWarnings("unused") InterruptedException ex) {
236 exec.shutdownNow();
237 Thread.currentThread().interrupt();
238 }
239 }
240 }
241 }