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.taskdef; 021 022import ca.uhn.fhir.i18n.Msg; 023import ca.uhn.fhir.util.StopWatch; 024import ca.uhn.fhir.util.VersionEnum; 025import com.google.common.collect.ForwardingMap; 026import org.apache.commons.lang3.concurrent.BasicThreadFactory; 027import org.slf4j.Logger; 028import org.slf4j.LoggerFactory; 029import org.springframework.jdbc.core.ColumnMapRowMapper; 030import org.springframework.jdbc.core.JdbcTemplate; 031import org.springframework.jdbc.core.RowCallbackHandler; 032 033import java.sql.ResultSet; 034import java.sql.SQLException; 035import java.util.ArrayList; 036import java.util.Date; 037import java.util.HashMap; 038import java.util.List; 039import java.util.Map; 040import java.util.concurrent.Future; 041import java.util.concurrent.LinkedBlockingQueue; 042import java.util.concurrent.RejectedExecutionException; 043import java.util.concurrent.RejectedExecutionHandler; 044import java.util.concurrent.ThreadPoolExecutor; 045import java.util.concurrent.TimeUnit; 046import java.util.function.Function; 047 048public abstract class BaseColumnCalculatorTask extends BaseTableColumnTask { 049 050 protected static final Logger ourLog = LoggerFactory.getLogger(BaseColumnCalculatorTask.class); 051 private int myBatchSize = 10000; 052 private ThreadPoolExecutor myExecutor; 053 private String myPidColumnName; 054 055 /** 056 * Constructor 057 */ 058 public BaseColumnCalculatorTask(VersionEnum theRelease, String theVersion) { 059 this(theRelease.toString(), theVersion); 060 } 061 062 /** 063 * Constructor 064 */ 065 public BaseColumnCalculatorTask(String theRelease, String theVersion) { 066 super(theRelease, theVersion); 067 } 068 069 public void setBatchSize(int theBatchSize) { 070 myBatchSize = theBatchSize; 071 } 072 073 /** 074 * Allows concrete implementations to decide if they should be skipped. 075 * 076 * @return a boolean indicating whether or not to skip execution of the task. 077 */ 078 protected abstract boolean shouldSkipTask(); 079 080 @Override 081 public synchronized void doExecute() throws SQLException { 082 if (isDryRun() || shouldSkipTask()) { 083 return; 084 } 085 086 initializeExecutor(); 087 088 try { 089 090 while (true) { 091 MyRowCallbackHandler rch = new MyRowCallbackHandler(); 092 getTxTemplate().execute(t -> { 093 JdbcTemplate jdbcTemplate = newJdbcTemplate(); 094 jdbcTemplate.setMaxRows(100000); 095 096 String sql = "SELECT * FROM " + getTableName() + " WHERE " + getWhereClause(); 097 logInfo( 098 ourLog, 099 "Finding up to {} rows in {} that requires calculations, using query: {}", 100 myBatchSize, 101 getTableName(), 102 sql); 103 104 jdbcTemplate.query(sql, rch); 105 rch.done(); 106 107 return null; 108 }); 109 110 rch.submitNext(); 111 List<Future<?>> futures = rch.getFutures(); 112 if (futures.isEmpty()) { 113 break; 114 } 115 116 logInfo(ourLog, "Waiting for {} tasks to complete", futures.size()); 117 for (Future<?> next : futures) { 118 try { 119 next.get(); 120 } catch (Exception e) { 121 throw new SQLException(Msg.code(69) + e); 122 } 123 } 124 } 125 126 } finally { 127 destroyExecutor(); 128 } 129 } 130 131 private void destroyExecutor() { 132 myExecutor.shutdownNow(); 133 } 134 135 private void initializeExecutor() { 136 int maximumPoolSize = Runtime.getRuntime().availableProcessors(); 137 138 LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<>(maximumPoolSize); 139 BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() 140 .namingPattern("worker-" + "-%d") 141 .daemon(false) 142 .priority(Thread.NORM_PRIORITY) 143 .build(); 144 RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() { 145 @Override 146 public void rejectedExecution(Runnable theRunnable, ThreadPoolExecutor theExecutor) { 147 logInfo( 148 ourLog, 149 "Note: Executor queue is full ({} elements), waiting for a slot to become available!", 150 executorQueue.size()); 151 StopWatch sw = new StopWatch(); 152 try { 153 executorQueue.put(theRunnable); 154 } catch (InterruptedException theE) { 155 throw new RejectedExecutionException( 156 Msg.code(70) + "Task " + theRunnable.toString() + " rejected from " + theE.toString()); 157 } 158 logInfo(ourLog, "Slot become available after {}ms", sw.getMillis()); 159 } 160 }; 161 myExecutor = new ThreadPoolExecutor( 162 maximumPoolSize, 163 maximumPoolSize, 164 0L, 165 TimeUnit.MILLISECONDS, 166 executorQueue, 167 threadFactory, 168 rejectedExecutionHandler); 169 } 170 171 public void setPidColumnName(String thePidColumnName) { 172 myPidColumnName = thePidColumnName; 173 } 174 175 private Future<?> updateRows(List<Map<String, Object>> theRows) { 176 Runnable task = () -> { 177 StopWatch sw = new StopWatch(); 178 getTxTemplate().execute(t -> { 179 180 // Loop through rows 181 assert theRows != null; 182 for (Map<String, Object> nextRow : theRows) { 183 184 Map<String, Object> newValues = new HashMap<>(); 185 MandatoryKeyMap<String, Object> nextRowMandatoryKeyMap = new MandatoryKeyMap<>(nextRow); 186 187 // Apply calculators 188 for (Map.Entry<String, Function<MandatoryKeyMap<String, Object>, Object>> nextCalculatorEntry : 189 myCalculators.entrySet()) { 190 String nextColumn = nextCalculatorEntry.getKey(); 191 Function<MandatoryKeyMap<String, Object>, Object> nextCalculator = 192 nextCalculatorEntry.getValue(); 193 Object value = nextCalculator.apply(nextRowMandatoryKeyMap); 194 newValues.put(nextColumn, value); 195 } 196 197 // Generate update SQL 198 StringBuilder sqlBuilder = new StringBuilder(); 199 List<Object> arguments = new ArrayList<>(); 200 sqlBuilder.append("UPDATE "); 201 sqlBuilder.append(getTableName()); 202 sqlBuilder.append(" SET "); 203 for (Map.Entry<String, Object> nextNewValueEntry : newValues.entrySet()) { 204 if (arguments.size() > 0) { 205 sqlBuilder.append(", "); 206 } 207 sqlBuilder.append(nextNewValueEntry.getKey()).append(" = ?"); 208 arguments.add(nextNewValueEntry.getValue()); 209 } 210 sqlBuilder.append(" WHERE " + myPidColumnName + " = ?"); 211 arguments.add((Number) nextRow.get(myPidColumnName)); 212 213 // Apply update SQL 214 newJdbcTemplate().update(sqlBuilder.toString(), arguments.toArray()); 215 } 216 return theRows.size(); 217 }); 218 logInfo(ourLog, "Updated {} rows on {} in {}", theRows.size(), getTableName(), sw.toString()); 219 }; 220 return myExecutor.submit(task); 221 } 222 223 public static class MandatoryKeyMap<K, V> extends ForwardingMap<K, V> { 224 225 private final Map<K, V> myWrap; 226 227 public MandatoryKeyMap(Map<K, V> theWrap) { 228 myWrap = theWrap; 229 } 230 231 @Override 232 public V get(Object theKey) { 233 if (!containsKey(theKey)) { 234 throw new IllegalArgumentException(Msg.code(71) + "No key: " + theKey); 235 } 236 return super.get(theKey); 237 } 238 239 public String getString(String theKey) { 240 return (String) get(theKey); 241 } 242 243 public Date getDate(String theKey) { 244 return (Date) get(theKey); 245 } 246 247 @Override 248 protected Map<K, V> delegate() { 249 return myWrap; 250 } 251 252 public String getResourceType() { 253 return getString("RES_TYPE"); 254 } 255 256 public String getParamName() { 257 return getString("SP_NAME"); 258 } 259 } 260 261 private class MyRowCallbackHandler implements RowCallbackHandler { 262 263 private List<Map<String, Object>> myRows = new ArrayList<>(); 264 private List<Future<?>> myFutures = new ArrayList<>(); 265 266 @Override 267 public void processRow(ResultSet rs) throws SQLException { 268 Map<String, Object> row = new ColumnMapRowMapper().mapRow(rs, 0); 269 myRows.add(row); 270 271 if (myRows.size() >= myBatchSize) { 272 submitNext(); 273 } 274 } 275 276 private void submitNext() { 277 if (myRows.size() > 0) { 278 myFutures.add(updateRows(myRows)); 279 myRows = new ArrayList<>(); 280 } 281 } 282 283 public List<Future<?>> getFutures() { 284 return myFutures; 285 } 286 287 public void done() { 288 if (myRows.size() > 0) { 289 submitNext(); 290 } 291 } 292 } 293}