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}