001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.jdbc.support;
018
019import java.sql.Connection;
020import java.sql.SQLException;
021import java.sql.Statement;
022import java.util.concurrent.TimeUnit;
023
024import javax.sql.DataSource;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028
029import org.springframework.beans.factory.InitializingBean;
030import org.springframework.jdbc.CannotGetJdbcConnectionException;
031import org.springframework.lang.Nullable;
032
033/**
034 * Bean that checks if a database has already started up. To be referenced
035 * via "depends-on" from beans that depend on database startup, like a Hibernate
036 * SessionFactory or custom data access objects that access a DataSource directly.
037 *
038 * <p>Useful to defer application initialization until a database has started up.
039 * Particularly appropriate for waiting on a slowly starting Oracle database.
040 *
041 * @author Juergen Hoeller
042 * @since 18.12.2003
043 */
044public class DatabaseStartupValidator implements InitializingBean {
045
046        /**
047         * The default interval.
048         */
049        public static final int DEFAULT_INTERVAL = 1;
050
051        /**
052         * The default timeout.
053         */
054        public static final int DEFAULT_TIMEOUT = 60;
055
056
057        protected final Log logger = LogFactory.getLog(getClass());
058
059        @Nullable
060        private DataSource dataSource;
061
062        @Nullable
063        private String validationQuery;
064
065        private int interval = DEFAULT_INTERVAL;
066
067        private int timeout = DEFAULT_TIMEOUT;
068
069
070        /**
071         * Set the DataSource to validate.
072         */
073        public void setDataSource(DataSource dataSource) {
074                this.dataSource = dataSource;
075        }
076
077        /**
078         * Set the SQL query string to use for validation.
079         */
080        public void setValidationQuery(String validationQuery) {
081                this.validationQuery = validationQuery;
082        }
083
084        /**
085         * Set the interval between validation runs (in seconds).
086         * Default is {@value #DEFAULT_INTERVAL}.
087         */
088        public void setInterval(int interval) {
089                this.interval = interval;
090        }
091
092        /**
093         * Set the timeout (in seconds) after which a fatal exception
094         * will be thrown. Default is {@value #DEFAULT_TIMEOUT}.
095         */
096        public void setTimeout(int timeout) {
097                this.timeout = timeout;
098        }
099
100
101        /**
102         * Check whether the validation query can be executed on a Connection
103         * from the specified DataSource, with the specified interval between
104         * checks, until the specified timeout.
105         */
106        @Override
107        public void afterPropertiesSet() {
108                if (this.dataSource == null) {
109                        throw new IllegalArgumentException("Property 'dataSource' is required");
110                }
111                if (this.validationQuery == null) {
112                        throw new IllegalArgumentException("Property 'validationQuery' is required");
113                }
114
115                try {
116                        boolean validated = false;
117                        long beginTime = System.currentTimeMillis();
118                        long deadLine = beginTime + TimeUnit.SECONDS.toMillis(this.timeout);
119                        SQLException latestEx = null;
120
121                        while (!validated && System.currentTimeMillis() < deadLine) {
122                                Connection con = null;
123                                Statement stmt = null;
124                                try {
125                                        con = this.dataSource.getConnection();
126                                        if (con == null) {
127                                                throw new CannotGetJdbcConnectionException("Failed to execute validation query: " +
128                                                                "DataSource returned null from getConnection(): " + this.dataSource);
129                                        }
130                                        stmt = con.createStatement();
131                                        stmt.execute(this.validationQuery);
132                                        validated = true;
133                                }
134                                catch (SQLException ex) {
135                                        latestEx = ex;
136                                        if (logger.isDebugEnabled()) {
137                                                logger.debug("Validation query [" + this.validationQuery + "] threw exception", ex);
138                                        }
139                                        if (logger.isInfoEnabled()) {
140                                                float rest = ((float) (deadLine - System.currentTimeMillis())) / 1000;
141                                                if (rest > this.interval) {
142                                                        logger.info("Database has not started up yet - retrying in " + this.interval +
143                                                                        " seconds (timeout in " + rest + " seconds)");
144                                                }
145                                        }
146                                }
147                                finally {
148                                        JdbcUtils.closeStatement(stmt);
149                                        JdbcUtils.closeConnection(con);
150                                }
151
152                                if (!validated) {
153                                        TimeUnit.SECONDS.sleep(this.interval);
154                                }
155                        }
156
157                        if (!validated) {
158                                throw new CannotGetJdbcConnectionException(
159                                                "Database has not started up within " + this.timeout + " seconds", latestEx);
160                        }
161
162                        if (logger.isInfoEnabled()) {
163                                float duration = ((float) (System.currentTimeMillis() - beginTime)) / 1000;
164                                logger.info("Database startup detected after " + duration + " seconds");
165                        }
166                }
167                catch (InterruptedException ex) {
168                        // Re-interrupt current thread, to allow other threads to react.
169                        Thread.currentThread().interrupt();
170                }
171        }
172
173}