001/*-
002 * #%L
003 * HAPI FHIR Server - SQL Migration
004 * %%
005 * Copyright (C) 2014 - 2023 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.jpa.migrate;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.jpa.migrate.entity.HapiMigrationEntity;
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027import java.util.Optional;
028import java.util.UUID;
029
030import static org.apache.commons.lang3.StringUtils.isBlank;
031
032/**
033 * The approach used in this class is borrowed from org.flywaydb.community.database.ignite.thin.IgniteThinDatabase
034 */
035public class HapiMigrationLock implements AutoCloseable {
036        public static final Integer LOCK_PID = -100;
037        private static final Logger ourLog = LoggerFactory.getLogger(HapiMigrationLock.class);
038        public static final int SLEEP_MILLIS_BETWEEN_LOCK_RETRIES = 1000;
039        public static final int DEFAULT_MAX_RETRY_ATTEMPTS = 50;
040        public static int ourMaxRetryAttempts = DEFAULT_MAX_RETRY_ATTEMPTS;
041        public static final String CLEAR_LOCK_TABLE_WITH_DESCRIPTION = "CLEAR_LOCK_TABLE_WITH_DESCRIPTION";
042
043        private final String myLockDescription = UUID.randomUUID().toString();
044
045        private final HapiMigrationStorageSvc myMigrationStorageSvc;
046
047        /**
048         * This constructor should only ever be called from within a try-with-resources so the lock is released when the block is exited
049         */
050        public HapiMigrationLock(HapiMigrationStorageSvc theMigrationStorageSvc) {
051                myMigrationStorageSvc = theMigrationStorageSvc;
052                lock();
053        }
054
055        private void lock() {
056                cleanLockTableIfRequested();
057
058                int retryCount = 0;
059                do {
060                        try {
061                                if (insertLockingRow()) {
062                                        return;
063                                }
064                                retryCount++;
065
066                                if (retryCount < ourMaxRetryAttempts) {
067                                        ourLog.info("Waiting for lock on {}.  Retry {}/{}", myMigrationStorageSvc.getMigrationTablename(), retryCount, ourMaxRetryAttempts);
068                                        Thread.sleep(SLEEP_MILLIS_BETWEEN_LOCK_RETRIES);
069                                }
070                        } catch (InterruptedException ex) {
071                                // Ignore - if interrupted, we still need to wait for lock to become available
072                        }
073                } while (retryCount < ourMaxRetryAttempts);
074
075                String message = "Unable to obtain table lock - another database migration may be running.  If no " +
076                        "other database migration is running, then the previous migration did not shut down properly and the " +
077                        "lock record needs to be deleted manually.  The lock record is located in the " + myMigrationStorageSvc.getMigrationTablename() + " table with " +
078                        "INSTALLED_RANK = " + LOCK_PID;
079
080                Optional<HapiMigrationEntity> otherLockFound = myMigrationStorageSvc.findFirstByPidAndNotDescription(LOCK_PID, myLockDescription);
081                if (otherLockFound.isPresent()) {
082                        message += " and DESCRIPTION = " + otherLockFound.get().getDescription();
083                }
084
085                throw new HapiMigrationException(Msg.code(2153) + message);
086        }
087
088        /**
089         *
090         * @return whether a lock record was successfully deleted
091         */
092        boolean cleanLockTableIfRequested() {
093                String description = System.getProperty(CLEAR_LOCK_TABLE_WITH_DESCRIPTION);
094                if (isBlank(description)) {
095                        description = System.getenv(CLEAR_LOCK_TABLE_WITH_DESCRIPTION);
096                }
097                if (isBlank(description)) {
098                        return false;
099                }
100
101                ourLog.info("Repairing lock table.  Removing row in " + myMigrationStorageSvc.getMigrationTablename() + " with INSTALLED_RANK = " + LOCK_PID + " and DESCRIPTION = " + description);
102                boolean result = myMigrationStorageSvc.deleteLockRecord(description);
103                if (result) {
104                        ourLog.info("Successfully removed lock record");
105                } else {
106                        ourLog.info("No lock record found");
107                }
108                return result;
109        }
110
111        private boolean insertLockingRow() {
112                try {
113                        boolean storedSuccessfully = myMigrationStorageSvc.insertLockRecord(myLockDescription);
114                        if (storedSuccessfully) {
115                                ourLog.info("Migration Lock Row added. [uuid={}]", myLockDescription);
116                        }
117                        return storedSuccessfully;
118                } catch (Exception e) {
119                        ourLog.debug("Failed to insert lock record: {}", e.getMessage());
120                        return false;
121                }
122        }
123
124        @Override
125        public void close() {
126                boolean result = myMigrationStorageSvc.deleteLockRecord(myLockDescription);
127                if (!result) {
128                        ourLog.error("Failed to delete migration lock record for description = [{}]", myLockDescription);
129                }
130        }
131
132        public static void setMaxRetryAttempts(int theMaxRetryAttempts) {
133                ourMaxRetryAttempts = theMaxRetryAttempts;
134        }
135}