001/*- 002 * #%L 003 * HAPI FHIR Server - SQL Migration 004 * %% 005 * Copyright (C) 2014 - 2025 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}