001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.activemq.store.jdbc;
018
019import java.io.IOException;
020import java.sql.Connection;
021import java.sql.PreparedStatement;
022import java.sql.ResultSet;
023import java.sql.SQLException;
024import java.sql.Timestamp;
025import java.util.Date;
026import java.util.concurrent.TimeUnit;
027import org.apache.activemq.util.IOExceptionSupport;
028import org.apache.activemq.util.ServiceStopper;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032/**
033 * Represents an exclusive lease on a database to avoid multiple brokers running
034 * against the same logical database.
035 * 
036 * @org.apache.xbean.XBean element="lease-database-locker"
037 * 
038 */
039public class LeaseDatabaseLocker extends AbstractJDBCLocker {
040    private static final Logger LOG = LoggerFactory.getLogger(LeaseDatabaseLocker.class);
041
042    protected int maxAllowableDiffFromDBTime = 0;
043    protected long diffFromCurrentTime = Long.MAX_VALUE;
044    protected String leaseHolderId;
045    protected boolean handleStartException;
046
047    public void doStart() throws Exception {
048
049        if (lockAcquireSleepInterval < lockable.getLockKeepAlivePeriod()) {
050            LOG.warn("LockableService keep alive period: " + lockable.getLockKeepAlivePeriod()
051                    + ", which renews the lease, is greater than lockAcquireSleepInterval: " + lockAcquireSleepInterval
052                    + ", the lease duration. These values will allow the lease to expire.");
053        }
054
055        LOG.info(getLeaseHolderId() + " attempting to acquire exclusive lease to become the master");
056        String sql = getStatements().getLeaseObtainStatement();
057        LOG.debug(getLeaseHolderId() + " locking Query is "+sql);
058
059        long now = 0l;
060        while (!isStopping()) {
061            Connection connection = null;
062            PreparedStatement statement = null;
063            try {
064                connection = getConnection();
065                initTimeDiff(connection);
066
067                statement = connection.prepareStatement(sql);
068                setQueryTimeout(statement);
069
070                now = System.currentTimeMillis() + diffFromCurrentTime;
071                statement.setString(1, getLeaseHolderId());
072                statement.setLong(2, now + lockAcquireSleepInterval);
073                statement.setLong(3, now);
074
075                int result = statement.executeUpdate();
076                if (result == 1) {
077                    // we got the lease, verify we still have it
078                    if (keepAlive()) {
079                        break;
080                    }
081                }
082
083                reportLeasOwnerShipAndDuration(connection);
084
085            } catch (Exception e) {
086                LOG.warn(getLeaseHolderId() + " lease acquire failure: "+ e, e);
087                if (isStopping()) {
088                    throw new Exception(
089                            "Cannot start broker as being asked to shut down. "
090                                    + "Interrupted attempt to acquire lock: "
091                                    + e, e);
092                }
093                if (handleStartException) {
094                    throw e;
095                }
096            } finally {
097                close(statement);
098                close(connection);
099            }
100
101            LOG.debug(getLeaseHolderId() + " failed to acquire lease.  Sleeping for " + lockAcquireSleepInterval + " milli(s) before trying again...");
102            TimeUnit.MILLISECONDS.sleep(lockAcquireSleepInterval);
103        }
104        if (isStopping()) {
105            throw new RuntimeException(getLeaseHolderId() + " failing lease acquire due to stop");
106        }
107
108        LOG.info(getLeaseHolderId() + ", becoming master with lease expiry " + new Date(now + lockAcquireSleepInterval) + " on dataSource: " + dataSource);
109    }
110
111    private void reportLeasOwnerShipAndDuration(Connection connection) throws SQLException {
112        PreparedStatement statement = null;
113        try {
114            statement = connection.prepareStatement(getStatements().getLeaseOwnerStatement());
115            ResultSet resultSet = statement.executeQuery();
116            while (resultSet.next()) {
117                LOG.debug(getLeaseHolderId() + " Lease held by " + resultSet.getString(1) + " till " + new Date(resultSet.getLong(2)));
118            }
119        } finally {
120            close(statement);
121        }
122    }
123
124    protected long initTimeDiff(Connection connection) throws SQLException {
125        if (Long.MAX_VALUE == diffFromCurrentTime) {
126            if (maxAllowableDiffFromDBTime > 0) {
127                diffFromCurrentTime = determineTimeDifference(connection);
128            } else {
129                diffFromCurrentTime = 0l;
130            }
131        }
132        return diffFromCurrentTime;
133    }
134
135    protected long determineTimeDifference(Connection connection) throws SQLException {
136        try (PreparedStatement statement = connection.prepareStatement(getStatements().getCurrentDateTime());
137             ResultSet resultSet = statement.executeQuery()) {
138            long result = 0l;
139            if (resultSet.next()) {
140                Timestamp timestamp = resultSet.getTimestamp(1);
141                long diff = System.currentTimeMillis() - timestamp.getTime();
142                if (Math.abs(diff) > maxAllowableDiffFromDBTime) {
143                    // off by more than maxAllowableDiffFromDBTime so lets adjust
144                    result = (-diff);
145                }
146                LOG.info(getLeaseHolderId() + " diff adjust from db: " + result + ", db time: " + timestamp);
147            }
148            return result;
149        }
150    }
151
152    public void doStop(ServiceStopper stopper) throws Exception {
153        if (lockable.getBrokerService() != null && lockable.getBrokerService().isRestartRequested()) {
154            // keep our lease for restart
155            return;
156        }
157        releaseLease();
158    }
159
160    private void releaseLease() {
161        Connection connection = null;
162        PreparedStatement statement = null;
163        try {
164            connection = getConnection();
165            statement = connection.prepareStatement(getStatements().getLeaseUpdateStatement());
166            statement.setString(1, null);
167            statement.setLong(2, 0l);
168            statement.setString(3, getLeaseHolderId());
169            if (statement.executeUpdate() == 1) {
170                LOG.info(getLeaseHolderId() + ", released lease");
171            }
172        } catch (Exception e) {
173            LOG.error(getLeaseHolderId() + " failed to release lease: " + e, e);
174        } finally {
175            close(statement);
176            close(connection);
177        }
178    }
179
180    @Override
181    public boolean keepAlive() throws IOException {
182        boolean result = false;
183        final String sql = getStatements().getLeaseUpdateStatement();
184        LOG.debug(getLeaseHolderId() + ", lease keepAlive Query is " + sql);
185
186        Connection connection = null;
187        PreparedStatement statement = null;
188        try {
189            connection = getConnection();
190
191            initTimeDiff(connection);
192            statement = connection.prepareStatement(sql);
193            setQueryTimeout(statement);
194
195            final long now = System.currentTimeMillis() + diffFromCurrentTime;
196            statement.setString(1, getLeaseHolderId());
197            statement.setLong(2, now + lockAcquireSleepInterval);
198            statement.setString(3, getLeaseHolderId());
199
200            result = (statement.executeUpdate() == 1);
201
202            if (!result) {
203                reportLeasOwnerShipAndDuration(connection);
204            }
205        } catch (Exception e) {
206            LOG.warn(getLeaseHolderId() + ", failed to update lease: " + e, e);
207            IOException ioe = IOExceptionSupport.create(e);
208            lockable.getBrokerService().handleIOException(ioe);
209            throw ioe;
210        } finally {
211            close(statement);
212            close(connection);
213        }
214        return result;
215    }
216
217    public String getLeaseHolderId() {
218        if (leaseHolderId == null) {
219            if (lockable.getBrokerService() != null) {
220                leaseHolderId = lockable.getBrokerService().getBrokerName();
221            }
222        }
223        return leaseHolderId;
224    }
225
226    public void setLeaseHolderId(String leaseHolderId) {
227        this.leaseHolderId = leaseHolderId;
228    }
229
230    public int getMaxAllowableDiffFromDBTime() {
231        return maxAllowableDiffFromDBTime;
232    }
233
234    public void setMaxAllowableDiffFromDBTime(int maxAllowableDiffFromDBTime) {
235        this.maxAllowableDiffFromDBTime = maxAllowableDiffFromDBTime;
236    }
237
238    public boolean isHandleStartException() {
239        return handleStartException;
240    }
241
242    public void setHandleStartException(boolean handleStartException) {
243        this.handleStartException = handleStartException;
244    }
245
246    @Override
247    public String toString() {
248        return "LeaseDatabaseLocker owner:" + leaseHolderId + ",duration:" + lockAcquireSleepInterval + ",renew:" + lockAcquireSleepInterval;
249    }
250}