001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.NotThreadSafe;
023import javax.annotation.concurrent.ThreadSafe;
024import java.time.Duration;
025
026import static java.util.Objects.requireNonNull;
027
028/**
029 * Immutable startup and shutdown deadline policy shared by every configured
030 * lifecycle component.
031 * <p>
032 * Every timeout accepts {@link Duration#ZERO} as an immediate deadline, not
033 * an unlimited wait or a disabled phase. Zero leaves no waiting allowance for
034 * startup, startup cancelation, graceful drain or forced termination proof.
035 * Use positive budgets for normal operation; shutdown completion depends on
036 * physical termination evidence, not merely requesting interruption.
037 * <p>
038 * Each timeout and the sum of startup cancelation, graceful shutdown, and
039 * forced shutdown timeouts must fit in signed 64-bit nanoseconds. These three
040 * phases share one shutdown-intent origin; startup has its own independent
041 * budget. The combined bound keeps monotonic deadline comparisons valid even
042 * when the clock wraps.
043 */
044@ThreadSafe
045public final class LifecyclePolicy {
046        @NonNull
047        private static final Duration DEFAULT_STARTUP_TIMEOUT = Duration.ofSeconds(30);
048        @NonNull
049        private static final Duration DEFAULT_STARTUP_CANCELATION_TIMEOUT =
050                        Duration.ofSeconds(2);
051        @NonNull
052        private static final Duration DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT =
053                        Duration.ofSeconds(15);
054        @NonNull
055        private static final Duration DEFAULT_FORCED_SHUTDOWN_TIMEOUT =
056                        Duration.ofSeconds(3);
057        @NonNull
058        private static final LifecyclePolicy DEFAULT_INSTANCE = builder().build();
059
060        @NonNull
061        private final Duration startupTimeout;
062        @NonNull
063        private final Duration startupCancelationTimeout;
064        @NonNull
065        private final Duration gracefulShutdownTimeout;
066        @NonNull
067        private final Duration forcedShutdownTimeout;
068
069        private LifecyclePolicy(@NonNull Builder builder) {
070                Builder exactBuilder = requireNonNull(builder);
071                this.startupTimeout = validate(exactBuilder.startupTimeout,
072                                "startupTimeout");
073                this.startupCancelationTimeout = validate(
074                                exactBuilder.startupCancelationTimeout,
075                                "startupCancelationTimeout");
076                this.gracefulShutdownTimeout = validate(
077                                exactBuilder.gracefulShutdownTimeout,
078                                "gracefulShutdownTimeout");
079                this.forcedShutdownTimeout = validate(
080                                exactBuilder.forcedShutdownTimeout,
081                                "forcedShutdownTimeout");
082                LifecycleDeadlines.validateShutdownHorizon(this.startupCancelationTimeout,
083                                this.gracefulShutdownTimeout, this.forcedShutdownTimeout);
084        }
085
086        /** @return the shared default lifecycle policy */
087        @NonNull
088        public static LifecyclePolicy defaultInstance() {
089                return DEFAULT_INSTANCE;
090        }
091
092        /** @return a mutable builder initialized with the default policy */
093        @NonNull
094        public static Builder builder() {
095                return new Builder();
096        }
097
098        /** @return startup timeout */
099        @NonNull
100        public Duration getStartupTimeout() {
101                return this.startupTimeout;
102        }
103
104        /** @return budget for canceling an in-flight startup call */
105        @NonNull
106        public Duration getStartupCancelationTimeout() {
107                return this.startupCancelationTimeout;
108        }
109
110        /** @return graceful shutdown timeout */
111        @NonNull
112        public Duration getGracefulShutdownTimeout() {
113                return this.gracefulShutdownTimeout;
114        }
115
116        /** @return forced shutdown timeout */
117        @NonNull
118        public Duration getForcedShutdownTimeout() {
119                return this.forcedShutdownTimeout;
120        }
121
122        /** @return whether this policy is structurally equal to the other policy */
123        @Override
124        public boolean equals(@Nullable Object other) {
125                return this == other
126                                || other instanceof LifecyclePolicy policy
127                                && this.startupTimeout.equals(policy.startupTimeout)
128                                && this.startupCancelationTimeout.equals(
129                                                policy.startupCancelationTimeout)
130                                && this.gracefulShutdownTimeout.equals(
131                                                policy.gracefulShutdownTimeout)
132                                && this.forcedShutdownTimeout.equals(
133                                                policy.forcedShutdownTimeout);
134        }
135
136        /** @return structural lifecycle-policy hash code */
137        @Override
138        public int hashCode() {
139                int result = this.startupTimeout.hashCode();
140                result = 31 * result + this.startupCancelationTimeout.hashCode();
141                result = 31 * result + this.gracefulShutdownTimeout.hashCode();
142                result = 31 * result + this.forcedShutdownTimeout.hashCode();
143                return result;
144        }
145
146        @NonNull
147        InternalLifecyclePolicy toInternal() {
148                return new InternalLifecyclePolicy(getStartupTimeout(),
149                                getStartupCancelationTimeout(), getGracefulShutdownTimeout(),
150                                getForcedShutdownTimeout());
151        }
152
153        @NonNull
154        static LifecyclePolicy fromInternal(
155                        @NonNull InternalLifecyclePolicy lifecyclePolicy) {
156                requireNonNull(lifecyclePolicy);
157                return builder()
158                                .startupTimeout(lifecyclePolicy.startupTimeout())
159                                .startupCancelationTimeout(
160                                                lifecyclePolicy.startupCancelationTimeout())
161                                .gracefulShutdownTimeout(
162                                                lifecyclePolicy.gracefulShutdownTimeout())
163                                .forcedShutdownTimeout(
164                                                lifecyclePolicy.forcedShutdownTimeout())
165                                .build();
166        }
167
168        @NonNull
169        private static Duration validate(@NonNull Duration duration,
170                        @NonNull String name) {
171                requireNonNull(duration, requireNonNull(name));
172                if (duration.isNegative())
173                        throw new IllegalArgumentException(name + " must be >= 0");
174                try {
175                        long validatedNanoseconds = duration.toNanos();
176                        if (validatedNanoseconds < 0L)
177                                throw new IllegalArgumentException(name + " must be >= 0");
178                } catch (ArithmeticException exception) {
179                        throw new IllegalArgumentException(
180                                        name + " exceeds signed nanoseconds", exception);
181                }
182                return duration;
183        }
184
185        /** Mutable builder for an immutable {@link LifecyclePolicy}. */
186        @NotThreadSafe
187        public static final class Builder {
188                @NonNull
189                private Duration startupTimeout;
190                @NonNull
191                private Duration startupCancelationTimeout;
192                @NonNull
193                private Duration gracefulShutdownTimeout;
194                @NonNull
195                private Duration forcedShutdownTimeout;
196
197                private Builder() {
198                        this.startupTimeout = DEFAULT_STARTUP_TIMEOUT;
199                        this.startupCancelationTimeout =
200                                        DEFAULT_STARTUP_CANCELATION_TIMEOUT;
201                        this.gracefulShutdownTimeout = DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT;
202                        this.forcedShutdownTimeout = DEFAULT_FORCED_SHUTDOWN_TIMEOUT;
203                }
204
205                /**
206                 * Sets the startup timeout. Zero means an immediate boundary.
207                 * A zero budget leaves no waiting allowance for framework setup or
208                 * transport startup and can immediately produce
209                 * {@link StartupDisposition#TIMED_OUT}; it does not disable the timeout.
210                 *
211                 * Passing {@code null} restores the built-in default of 30 seconds.
212                 *
213                 * @param startupTimeout startup timeout, or {@code null} to use the default
214                 * @return this builder
215                 * @throws IllegalArgumentException if negative or not representable as
216                 * signed nanoseconds
217                 */
218                @NonNull
219                public Builder startupTimeout(@Nullable Duration startupTimeout) {
220                        this.startupTimeout = startupTimeout == null
221                                        ? DEFAULT_STARTUP_TIMEOUT
222                                        : validate(startupTimeout, "startupTimeout");
223                        return this;
224                }
225
226                /**
227                 * Sets the startup-cancelation budget. Zero means an immediate boundary.
228                 * A zero budget leaves no waiting allowance for an active startup call
229                 * to return after shutdown intent. Remaining startup work stays tracked
230                 * through the graceful and forced shutdown phases.
231                 *
232                 * Passing {@code null} restores the built-in default of 2 seconds.
233                 *
234                 * @param startupCancelationTimeout cancelation budget, or {@code null}
235                 * to use the default
236                 * @return this builder
237                 * @throws IllegalArgumentException if negative or not representable as
238                 * signed nanoseconds
239                 */
240                @NonNull
241                public Builder startupCancelationTimeout(
242                                @Nullable Duration startupCancelationTimeout) {
243                        this.startupCancelationTimeout = startupCancelationTimeout == null
244                                        ? DEFAULT_STARTUP_CANCELATION_TIMEOUT
245                                        : validate(startupCancelationTimeout,
246                                                        "startupCancelationTimeout");
247                        return this;
248                }
249
250                /**
251                 * Sets the graceful shutdown timeout. Zero means an immediate boundary.
252                 * Passing {@code null} restores the built-in default of 15 seconds.
253                 * A zero budget leaves no waiting allowance for graceful drain;
254                 * participants without termination proof proceed to forced shutdown.
255                 * <p>
256                 * This budget is independent of the server request-handler timeout (60 seconds by default).
257                 * To let all permitted in-flight requests finish, allow their maximum remaining work and
258                 * response transmission time. A shorter drain intentionally permits interruption and
259                 * connection closure at the forced boundary. Interruption cannot guarantee that
260                 * noncooperative application code stops. Long-lived streams need an explicit termination
261                 * policy; the surrounding process/container budget must also include forced shutdown
262                 * and operational margin.
263                 * <p>
264                 * After a closing response, the standard HTTP server closes promptly when the
265                 * request was fully consumed and no surplus input was observed, including one
266                 * nonblocking check for input already queued on the socket. If unread input
267                 * may remain, its one-second lingering-input window consumes this budget even
268                 * after the response has been written. Short graceful budgets can therefore
269                 * proceed to forced shutdown while such a client connection still lingers.
270                 *
271                 * @param gracefulShutdownTimeout graceful shutdown timeout, or
272                 * {@code null} to use the default
273                 * @return this builder
274                 * @throws IllegalArgumentException if negative or not representable as
275                 * signed nanoseconds
276                 */
277                @NonNull
278                public Builder gracefulShutdownTimeout(
279                                @Nullable Duration gracefulShutdownTimeout) {
280                        this.gracefulShutdownTimeout = gracefulShutdownTimeout == null
281                                        ? DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT
282                                        : validate(gracefulShutdownTimeout,
283                                                        "gracefulShutdownTimeout");
284                        return this;
285                }
286
287                /**
288                 * Sets the forced shutdown timeout. Zero means an immediate boundary.
289                 * Passing {@code null} restores the built-in default of 3 seconds.
290                 * A zero budget leaves no waiting allowance for forced termination
291                 * proof. Any still-unproven or residual work produces an incomplete
292                 * result. In particular, zero graceful and forced budgets can make
293                 * even an idle running listener's shutdown incomplete because its stop
294                 * calls and termination proof are asynchronous.
295                 *
296                 * @param forcedShutdownTimeout forced shutdown timeout, or {@code null}
297                 * to use the default
298                 * @return this builder
299                 * @throws IllegalArgumentException if negative or not representable as
300                 * signed nanoseconds
301                 */
302                @NonNull
303                public Builder forcedShutdownTimeout(
304                                @Nullable Duration forcedShutdownTimeout) {
305                        this.forcedShutdownTimeout = forcedShutdownTimeout == null
306                                        ? DEFAULT_FORCED_SHUTDOWN_TIMEOUT
307                                        : validate(forcedShutdownTimeout, "forcedShutdownTimeout");
308                        return this;
309                }
310
311                /**
312                 * @return immutable lifecycle policy
313                 * @throws IllegalArgumentException if the combined startup-cancelation,
314                 * graceful-shutdown, and forced-shutdown budget exceeds signed nanoseconds
315                 */
316                @NonNull
317                public LifecyclePolicy build() {
318                        return new LifecyclePolicy(this);
319                }
320        }
321}