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