001/*-
002 * #%L
003 * HAPI FHIR JPA Server - Batch2 Task Processor
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.batch2.model;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.util.Logs;
024import com.google.common.collect.Maps;
025import jakarta.annotation.Nonnull;
026import org.slf4j.Logger;
027
028import java.util.Collections;
029import java.util.EnumMap;
030import java.util.EnumSet;
031import java.util.Map;
032import java.util.Set;
033
034/**
035 * Status of a Batch2 Job Instance.
036 * The initial state is QUEUED.
037 * The terminal states are COMPLETED, CANCELLED, or FAILED.
038 */
039public enum StatusEnum {
040
041        /**
042         * Task is waiting to execute and should begin with no intervention required.
043         */
044        QUEUED(true, false, true),
045
046        /**
047         * Task is current executing
048         */
049        IN_PROGRESS(true, false, true),
050
051        /**
052         * For reduction steps
053         */
054        FINALIZE(true, false, true),
055
056        /**
057         * Task completed successfully
058         */
059        COMPLETED(false, true, false),
060
061        /**
062         * Chunk execution resulted in an error but the error may be transient (or transient status is unknown).
063         * The job may still complete successfully.
064         * @deprecated this is basically a synonym for IN_PROGRESS - display should use the presence of an error message on the instance
065         * to indicate that there has been a transient error.
066         */
067        @Deprecated(since = "6.6")
068        // wipmb For 6.8 - remove all inbound transitions, and allow transition back to IN_PROGRESS. use message in ui to
069        // show danger status
070        ERRORED(true, false, true),
071
072        /**
073         * Task has failed and is known to be unrecoverable. There is no reason to believe that retrying will
074         * result in a different outcome.
075         */
076        FAILED(true, true, false),
077
078        /**
079         * Task has been cancelled by the user.
080         */
081        CANCELLED(true, true, false);
082
083        private static final Logger ourLog = Logs.getBatchTroubleshootingLog();
084
085        /** Map from state to Set of legal inbound states */
086        static final Map<StatusEnum, Set<StatusEnum>> ourFromStates;
087        /** Map from state to Set of legal outbound states */
088        static final Map<StatusEnum, Set<StatusEnum>> ourToStates;
089
090        static {
091                EnumMap<StatusEnum, Set<StatusEnum>> fromStates = new EnumMap<>(StatusEnum.class);
092                EnumMap<StatusEnum, Set<StatusEnum>> toStates = new EnumMap<>(StatusEnum.class);
093
094                for (StatusEnum nextEnum : StatusEnum.values()) {
095                        fromStates.put(nextEnum, EnumSet.noneOf(StatusEnum.class));
096                        toStates.put(nextEnum, EnumSet.noneOf(StatusEnum.class));
097                }
098                for (StatusEnum nextPriorEnum : StatusEnum.values()) {
099                        for (StatusEnum nextNextEnum : StatusEnum.values()) {
100                                if (isLegalStateTransition(nextPriorEnum, nextNextEnum)) {
101                                        fromStates.get(nextNextEnum).add(nextPriorEnum);
102                                        toStates.get(nextPriorEnum).add(nextNextEnum);
103                                }
104                        }
105                }
106
107                ourFromStates = Maps.immutableEnumMap(fromStates);
108                ourToStates = Maps.immutableEnumMap(toStates);
109        }
110
111        private final boolean myIncomplete;
112        private final boolean myEnded;
113        private final boolean myIsCancellable;
114        private static StatusEnum[] ourIncompleteStatuses;
115        private static Set<StatusEnum> ourEndedStatuses;
116        private static Set<StatusEnum> ourNotEndedStatuses;
117
118        StatusEnum(boolean theIncomplete, boolean theEnded, boolean theIsCancellable) {
119                myIncomplete = theIncomplete;
120                myEnded = theEnded;
121                myIsCancellable = theIsCancellable;
122        }
123
124        /**
125         * Statuses that represent a job that has not yet completed. I.e.
126         * all statuses except {@link #COMPLETED}
127         */
128        public static StatusEnum[] getIncompleteStatuses() {
129                StatusEnum[] retVal = ourIncompleteStatuses;
130                if (retVal == null) {
131                        EnumSet<StatusEnum> incompleteSet = EnumSet.noneOf(StatusEnum.class);
132                        for (StatusEnum next : values()) {
133                                if (next.myIncomplete) {
134                                        incompleteSet.add(next);
135                                }
136                        }
137                        ourIncompleteStatuses = incompleteSet.toArray(new StatusEnum[0]);
138                        retVal = ourIncompleteStatuses;
139                }
140                return retVal;
141        }
142
143        /**
144         * Statuses that represent a job that has ended. I.e.
145         * all statuses except {@link #QUEUED and #COMPLETED}
146         */
147        @Nonnull
148        public static Set<StatusEnum> getEndedStatuses() {
149                Set<StatusEnum> retVal = ourEndedStatuses;
150                if (retVal == null) {
151                        initializeStaticEndedStatuses();
152                }
153                retVal = ourEndedStatuses;
154                return retVal;
155        }
156
157        /**
158         * Statuses that represent a job that has not ended. I.e.
159         * {@link #QUEUED and #COMPLETED}
160         */
161        @Nonnull
162        public static Set<StatusEnum> getNotEndedStatuses() {
163                Set<StatusEnum> retVal = ourNotEndedStatuses;
164                if (retVal == null) {
165                        initializeStaticEndedStatuses();
166                }
167                retVal = ourNotEndedStatuses;
168                return retVal;
169        }
170
171        private static void initializeStaticEndedStatuses() {
172                EnumSet<StatusEnum> endedSet = EnumSet.noneOf(StatusEnum.class);
173                EnumSet<StatusEnum> notEndedSet = EnumSet.noneOf(StatusEnum.class);
174                for (StatusEnum next : values()) {
175                        if (next.myEnded) {
176                                endedSet.add(next);
177                        } else {
178                                notEndedSet.add(next);
179                        }
180                }
181                ourEndedStatuses = Collections.unmodifiableSet(endedSet);
182                ourNotEndedStatuses = Collections.unmodifiableSet(notEndedSet);
183        }
184
185        public static boolean isLegalStateTransition(StatusEnum theOrigStatus, StatusEnum theNewStatus) {
186                boolean canTransition;
187                switch (theOrigStatus) {
188                        case QUEUED:
189                                // initial state can transition to anything
190                                canTransition = true;
191                                break;
192                        case IN_PROGRESS:
193                        case ERRORED:
194                                canTransition = theNewStatus != QUEUED;
195                                break;
196                        case CANCELLED:
197                                // terminal state cannot transition
198                                canTransition = false;
199                                break;
200                        case COMPLETED:
201                                canTransition = false;
202                                break;
203                        case FAILED:
204                                canTransition = theNewStatus == FAILED;
205                                break;
206                        case FINALIZE:
207                                canTransition = theNewStatus != QUEUED && theNewStatus != IN_PROGRESS;
208                                break;
209                        default:
210                                throw new IllegalStateException(Msg.code(2131) + "Unknown batch state " + theOrigStatus);
211                }
212
213                if (!canTransition) {
214                        // we have a bug?
215                        ourLog.debug(
216                                        "Tried to execute an illegal state transition. [origStatus={}, newStatus={}]",
217                                        theOrigStatus,
218                                        theNewStatus);
219                }
220                return canTransition;
221        }
222
223        public boolean isIncomplete() {
224                return myIncomplete;
225        }
226
227        public boolean isEnded() {
228                return myEnded;
229        }
230
231        public boolean isCancellable() {
232                return myIsCancellable;
233        }
234
235        /**
236         * States that may transition to this state.
237         */
238        public Set<StatusEnum> getPriorStates() {
239                return ourFromStates.get(this);
240        }
241
242        /**
243         * States this state may transotion to.
244         */
245        public Set<StatusEnum> getNextStates() {
246                return ourToStates.get(this);
247        }
248}