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(
068                                                        "Waiting for lock on {}.  Retry {}/{}",
069                                                        myMigrationStorageSvc.getMigrationTablename(),
070                                                        retryCount,
071                                                        ourMaxRetryAttempts);
072                                        Thread.sleep(SLEEP_MILLIS_BETWEEN_LOCK_RETRIES);
073                                }
074                        } catch (InterruptedException ex) {
075                                // Ignore - if interrupted, we still need to wait for lock to become available
076                        }
077                } while (retryCount < ourMaxRetryAttempts);
078
079                String message = "Unable to obtain table lock - another database migration may be running.  If no "
080                                + "other database migration is running, then the previous migration did not shut down properly and the "
081                                + "lock record needs to be deleted manually.  The lock record is located in the "
082                                + myMigrationStorageSvc.getMigrationTablename() + " table with " + "INSTALLED_RANK = "
083                                + LOCK_PID;
084
085                Optional<HapiMigrationEntity> otherLockFound =
086                                myMigrationStorageSvc.findFirstByPidAndNotDescription(LOCK_PID, myLockDescription);
087                if (otherLockFound.isPresent()) {
088                        message += " and DESCRIPTION = " + otherLockFound.get().getDescription();
089                }
090
091                throw new HapiMigrationException(Msg.code(2153) + message);
092        }
093
094        /**
095         *
096         * @return whether a lock record was successfully deleted
097         */
098        boolean cleanLockTableIfRequested() {
099                String description = System.getProperty(CLEAR_LOCK_TABLE_WITH_DESCRIPTION);
100                if (isBlank(description)) {
101                        description = System.getenv(CLEAR_LOCK_TABLE_WITH_DESCRIPTION);
102                }
103                if (isBlank(description)) {
104                        return false;
105                }
106
107                ourLog.info("Repairing lock table.  Removing row in " + myMigrationStorageSvc.getMigrationTablename()
108                                + " with INSTALLED_RANK = " + LOCK_PID + " and DESCRIPTION = " + description);
109                boolean result = myMigrationStorageSvc.deleteLockRecord(description);
110                if (result) {
111                        ourLog.info("Successfully removed lock record");
112                } else {
113                        ourLog.info("No lock record found");
114                }
115                return result;
116        }
117
118        private boolean insertLockingRow() {
119                try {
120                        boolean storedSuccessfully = myMigrationStorageSvc.insertLockRecord(myLockDescription);
121                        if (storedSuccessfully) {
122                                ourLog.info("Migration Lock Row added. [uuid={}]", myLockDescription);
123                        }
124                        return storedSuccessfully;
125                } catch (Exception e) {
126                        ourLog.debug("Failed to insert lock record: {}", e.getMessage());
127                        return false;
128                }
129        }
130
131        @Override
132        public void close() {
133                boolean result = myMigrationStorageSvc.deleteLockRecord(myLockDescription);
134                if (!result) {
135                        ourLog.error("Failed to delete migration lock record for description = [{}]", myLockDescription);
136                }
137        }
138
139        public static void setMaxRetryAttempts(int theMaxRetryAttempts) {
140                ourMaxRetryAttempts = theMaxRetryAttempts;
141        }
142}