001package ca.uhn.fhir.jpa.migrate;
002
003/*-
004 * #%L
005 * HAPI FHIR Server - SQL Migration
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.jpa.migrate.entity.HapiMigrationEntity;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028import java.util.Optional;
029import java.util.UUID;
030
031import static org.apache.commons.lang3.StringUtils.isBlank;
032
033/**
034 * The approach used in this class is borrowed from org.flywaydb.community.database.ignite.thin.IgniteThinDatabase
035 */
036public class HapiMigrationLock implements AutoCloseable {
037        static final Integer LOCK_PID = -100;
038        private static final Logger ourLog = LoggerFactory.getLogger(HapiMigrationLock.class);
039        public static final int SLEEP_MILLIS_BETWEEN_LOCK_RETRIES = 1000;
040        public static final int DEFAULT_MAX_RETRY_ATTEMPTS = 50;
041        public static int ourMaxRetryAttempts = DEFAULT_MAX_RETRY_ATTEMPTS;
042        public static final String CLEAR_LOCK_TABLE_WITH_DESCRIPTION = "CLEAR_LOCK_TABLE_WITH_DESCRIPTION";
043
044        private final String myLockDescription = UUID.randomUUID().toString();
045
046        private final HapiMigrationStorageSvc myMigrationStorageSvc;
047
048        /**
049         * This constructor should only ever be called from within a try-with-resources so the lock is released when the block is exited
050         */
051        public HapiMigrationLock(HapiMigrationStorageSvc theMigrationStorageSvc) {
052                myMigrationStorageSvc = theMigrationStorageSvc;
053                lock();
054        }
055
056        private void lock() {
057                cleanLockTableIfRequested();
058
059                int retryCount = 0;
060                do {
061                        try {
062                                if (insertLockingRow()) {
063                                        return;
064                                }
065                                retryCount++;
066
067                                if (retryCount < ourMaxRetryAttempts) {
068                                        ourLog.info("Waiting for lock on {}.  Retry {}/{}", myMigrationStorageSvc.getMigrationTablename(), retryCount, ourMaxRetryAttempts);
069                                        Thread.sleep(SLEEP_MILLIS_BETWEEN_LOCK_RETRIES);
070                                }
071                        } catch (InterruptedException ex) {
072                                // Ignore - if interrupted, we still need to wait for lock to become available
073                        }
074                } while (retryCount < ourMaxRetryAttempts);
075
076                String message = "Unable to obtain table lock - another database migration may be running.  If no " +
077                        "other database migration is running, then the previous migration did not shut down properly and the " +
078                        "lock record needs to be deleted manually.  The lock record is located in the " + myMigrationStorageSvc.getMigrationTablename() + " table with " +
079                        "INSTALLED_RANK = " + LOCK_PID;
080
081                Optional<HapiMigrationEntity> otherLockFound = myMigrationStorageSvc.findFirstByPidAndNotDescription(LOCK_PID, myLockDescription);
082                if (otherLockFound.isPresent()) {
083                        message += " and DESCRIPTION = " + otherLockFound.get().getDescription();
084                }
085
086                throw new HapiMigrationException(Msg.code(2153) + message);
087        }
088
089        /**
090         *
091         * @return whether a lock record was successfully deleted
092         */
093        boolean cleanLockTableIfRequested() {
094                String description = System.getProperty(CLEAR_LOCK_TABLE_WITH_DESCRIPTION);
095                if (isBlank(description)) {
096                        description = System.getenv(CLEAR_LOCK_TABLE_WITH_DESCRIPTION);
097                }
098                if (isBlank(description)) {
099                        return false;
100                }
101
102                ourLog.info("Repairing lock table.  Removing row in " + myMigrationStorageSvc.getMigrationTablename() + " with INSTALLED_RANK = " + LOCK_PID + " and DESCRIPTION = " + description);
103                boolean result = myMigrationStorageSvc.deleteLockRecord(description);
104                if (result) {
105                        ourLog.info("Successfully removed lock record");
106                } else {
107                        ourLog.info("No lock record found");
108                }
109                return result;
110        }
111
112        private boolean insertLockingRow() {
113                try {
114                        return myMigrationStorageSvc.insertLockRecord(myLockDescription);
115                } catch (Exception e) {
116                        ourLog.debug("Failed to insert lock record: {}", e.getMessage());
117                        return false;
118                }
119        }
120
121        @Override
122        public void close() {
123                boolean result = myMigrationStorageSvc.deleteLockRecord(myLockDescription);
124                if (!result) {
125                        ourLog.error("Failed to delete migration lock record for description = [{}]", myLockDescription);
126                }
127        }
128
129        public static void setMaxRetryAttempts(int theMaxRetryAttempts) {
130                ourMaxRetryAttempts = theMaxRetryAttempts;
131        }
132}