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.ThreadSafe; 023import java.util.Collections; 024import java.util.EnumMap; 025import java.util.EnumSet; 026import java.util.List; 027import java.util.Map; 028import java.util.Optional; 029import java.util.Set; 030 031import static java.util.Objects.requireNonNull; 032 033/** 034 * Immutable result of one Soklet lifecycle attempt. 035 * <p> 036 * A result is complete when every configured lifecycle component has affirmative 037 * termination proof and no framework-tracked activity remains. Throwable 038 * instances retain their exact identities and may contain application-sensitive 039 * data; callers own any logging or disclosure decision. 040 */ 041@ThreadSafe 042public final class ShutdownResult { 043 @NonNull 044 private final ShutdownDisposition shutdownDisposition; 045 @NonNull 046 private final StartupDisposition startupDisposition; 047 @NonNull 048 private final List<@NonNull ShutdownComponentResult> 049 shutdownComponentResults; 050 @NonNull 051 private final Map<@NonNull ShutdownComponentType, 052 @NonNull ShutdownComponentResult> 053 shutdownComponentResultsByType; 054 @Nullable 055 private final Throwable startupFailureCause; 056 @Nullable 057 private final UnexpectedShutdownComponentTermination 058 unexpectedShutdownComponentTermination; 059 @NonNull 060 private final InternalShutdownResult internalResult; 061 062 private ShutdownResult(@NonNull InternalShutdownResult internalResult, 063 @Nullable Throwable startupFailureCause, 064 @Nullable ShutdownComponentType unexpectedShutdownComponentType, 065 @Nullable Throwable unexpectedCause) { 066 this.internalResult = requireNonNull(internalResult); 067 this.shutdownDisposition = ShutdownDisposition.valueOf( 068 internalResult.disposition().name()); 069 this.startupDisposition = StartupDisposition.valueOf( 070 internalResult.startupDisposition().name()); 071 String residualSummary = internalResult.retentionSummary() 072 .map(LifecycleRetentionSummary::summary).orElse(""); 073 this.shutdownComponentResults = internalResult.participantResults() 074 .stream() 075 .map(result -> fromInternal(result, residualSummary)).toList(); 076 EnumMap<ShutdownComponentType, ShutdownComponentResult> indexed = 077 new EnumMap<>(ShutdownComponentType.class); 078 for (ShutdownComponentResult result 079 : this.shutdownComponentResults) 080 indexed.put(result.getShutdownComponentType(), result); 081 this.shutdownComponentResultsByType = Collections.unmodifiableMap( 082 indexed); 083 this.startupFailureCause = startupFailureCause; 084 this.unexpectedShutdownComponentTermination = 085 unexpectedShutdownComponentType == null ? null 086 : new UnexpectedShutdownComponentTermination(unexpectedShutdownComponentType, 087 unexpectedCause); 088 } 089 090 @NonNull 091 static ShutdownResult fromInternal( 092 @NonNull InternalShutdownResult internalResult) { 093 return fromInternal(internalResult, null, null, null); 094 } 095 096 @NonNull 097 static ShutdownResult fromInternal( 098 @NonNull InternalShutdownResult internalResult, 099 @Nullable Throwable startupFailureCause, 100 @Nullable ShutdownComponentType unexpectedShutdownComponentType, 101 @Nullable Throwable unexpectedCause) { 102 return new ShutdownResult(internalResult, startupFailureCause, 103 unexpectedShutdownComponentType, unexpectedCause); 104 } 105 106 @NonNull 107 private static ShutdownComponentResult fromInternal( 108 @NonNull InternalLifecycleComponentShutdownResult internalResult, 109 @NonNull String residualSummary) { 110 Set<ResidualActivityType> residualActivityTypes = internalResult 111 .residualActivity().stream() 112 .map(kind -> ResidualActivityType.valueOf(kind.name())) 113 .collect(() -> EnumSet.noneOf(ResidualActivityType.class), 114 EnumSet::add, EnumSet::addAll); 115 ResidualActivityEvidence evidence = residualActivityTypes.isEmpty() ? null 116 : new ResidualActivityEvidence(residualActivityTypes, 117 residualSummary.isEmpty() 118 ? "Residual activity remains: " 119 + residualActivityTypes 120 : residualSummary); 121 return new ShutdownComponentResult( 122 ShutdownComponentType.valueOf(internalResult.kind().name()), 123 ShutdownComponentDisposition.valueOf( 124 internalResult.disposition().name()), 125 internalResult.failures(), evidence); 126 } 127 128 /** @return aggregate shutdown disposition */ 129 @NonNull 130 public ShutdownDisposition getShutdownDisposition() { 131 return this.shutdownDisposition; 132 } 133 134 /** @return startup disposition for the same lifecycle attempt */ 135 @NonNull 136 public StartupDisposition getStartupDisposition() { 137 return this.startupDisposition; 138 } 139 140 /** 141 * @return immutable shutdown component results in 142 * {@link ShutdownComponentType} enum order 143 */ 144 @NonNull 145 public List<@NonNull ShutdownComponentResult> getShutdownComponentResults() { 146 return this.shutdownComponentResults; 147 } 148 149 /** 150 * Looks up terminal evidence for one configured shutdown component type. 151 * 152 * @param shutdownComponentType shutdown component type 153 * @return shutdown component result, otherwise empty when that type 154 * was not configured 155 * @throws NullPointerException if {@code shutdownComponentType} is null 156 */ 157 @NonNull 158 public Optional<@NonNull ShutdownComponentResult> getShutdownComponentResult( 159 @NonNull ShutdownComponentType shutdownComponentType) { 160 return Optional.ofNullable(this.shutdownComponentResultsByType.get( 161 requireNonNull(shutdownComponentType))); 162 } 163 164 /** 165 * @return the cause associated with a failed, timed-out, or canceled startup 166 * attempt, otherwise empty 167 */ 168 @NonNull 169 public Optional<@NonNull Throwable> getStartupFailureCause() { 170 return Optional.ofNullable(this.startupFailureCause); 171 } 172 173 /** @return the first premature shutdown component termination, otherwise empty */ 174 @NonNull 175 public Optional<@NonNull UnexpectedShutdownComponentTermination> 176 getUnexpectedShutdownComponentTermination() { 177 return Optional.ofNullable(this.unexpectedShutdownComponentTermination); 178 } 179 180 /** 181 * @return {@code true} exactly when the disposition is not 182 * {@link ShutdownDisposition#INCOMPLETE} 183 */ 184 @NonNull 185 public Boolean isComplete() { 186 return getShutdownDisposition() != ShutdownDisposition.INCOMPLETE; 187 } 188 189 /** 190 * Returns a bounded, single-line diagnostic with dispositions, component 191 * results, failure presence and residual-component counts when available. 192 * Does not render Throwable instances, residual-summary text or retained 193 * runtime objects. Inspect the typed accessors for full evidence. 194 * 195 * @return compact lifecycle diagnostic; not a serialization format 196 */ 197 @Override 198 @NonNull 199 public String toString() { 200 StringBuilder diagnostic = new StringBuilder("ShutdownResult{shutdownDisposition=") 201 .append(this.shutdownDisposition.name()) 202 .append(", startupDisposition=").append(this.startupDisposition.name()) 203 .append(", shutdownComponentResults=").append(this.shutdownComponentResults) 204 .append(", startupFailurePresent=").append(this.startupFailureCause != null) 205 .append(", unexpectedShutdownComponentType=") 206 .append(this.unexpectedShutdownComponentTermination == null ? "none" 207 : this.unexpectedShutdownComponentTermination.getShutdownComponentType().name()); 208 this.internalResult.retentionSummary().ifPresent(retention -> diagnostic 209 .append(", residualComponentCounts=").append(retention.counts())); 210 return diagnostic.append('}').toString(); 211 } 212 213 @NonNull 214 InternalShutdownResult internalResult() { 215 return this.internalResult; 216 } 217}