001package io.ebean.config.dbplatform;
002
003import io.ebean.BackgroundExecutor;
004import io.ebean.Transaction;
005import io.ebean.util.JdbcClose;
006import org.slf4j.Logger;
007import org.slf4j.LoggerFactory;
008
009import javax.persistence.PersistenceException;
010import javax.sql.DataSource;
011import java.sql.Connection;
012import java.sql.PreparedStatement;
013import java.sql.ResultSet;
014import java.sql.SQLException;
015import java.util.Collections;
016import java.util.List;
017import java.util.NavigableSet;
018import java.util.TreeSet;
019import java.util.concurrent.atomic.AtomicBoolean;
020import java.util.concurrent.locks.ReentrantLock;
021
022/**
023 * Database sequence based IdGenerator.
024 */
025public abstract class SequenceIdGenerator implements PlatformIdGenerator {
026
027  protected static final Logger logger = LoggerFactory.getLogger("io.ebean.SEQ");
028
029  private final ReentrantLock lock = new ReentrantLock();
030
031  /**
032   * The actual sequence name.
033   */
034  protected final String seqName;
035
036  protected final DataSource dataSource;
037
038  protected final BackgroundExecutor backgroundExecutor;
039
040  protected final NavigableSet<Long> idList = new TreeSet<>();
041
042  protected final int allocationSize;
043
044  protected AtomicBoolean currentlyBackgroundLoading = new AtomicBoolean(false);
045
046  /**
047   * Construct given a dataSource and sql to return the next sequence value.
048   */
049  protected SequenceIdGenerator(BackgroundExecutor be, DataSource ds, String seqName, int allocationSize) {
050    this.backgroundExecutor = be;
051    this.dataSource = ds;
052    this.seqName = seqName;
053    this.allocationSize = allocationSize;
054  }
055
056  public abstract String getSql(int batchSize);
057
058  /**
059   * Returns the sequence name.
060   */
061  @Override
062  public String getName() {
063    return seqName;
064  }
065
066  /**
067   * Returns true.
068   */
069  @Override
070  public boolean isDbSequence() {
071    return true;
072  }
073
074  /**
075   * If allocateSize is large load some sequences in a background thread.
076   * <p>
077   * For example, when inserting a bean with a cascade on a OneToMany with many
078   * beans Ebean can call this to ensure .
079   * </p>
080   */
081  @Override
082  public void preAllocateIds(int requestSize) {
083    // do nothing by default
084  }
085
086  /**
087   * Return the next Id.
088   * <p>
089   * If a Transaction has been passed in use the Connection from it.
090   * </p>
091   */
092  @Override
093  public Object nextId(Transaction t) {
094    lock.lock();
095    try {
096      int size = idList.size();
097      if (size > 0) {
098        maybeLoadMoreInBackground(size);
099      } else {
100        loadMore(allocationSize);
101      }
102      return idList.pollFirst();
103    } finally {
104      lock.unlock();
105    }
106  }
107
108  private void maybeLoadMoreInBackground(int currentSize) {
109    if (allocationSize > 1) {
110      if (currentSize <= allocationSize / 2) {
111        loadInBackground(allocationSize);
112      }
113    }
114  }
115
116  private void loadMore(int requestSize) {
117    List<Long> newIds = getMoreIds(requestSize);
118    lock.lock();
119    try {
120      idList.addAll(newIds);
121    } finally {
122      lock.unlock();
123    }
124  }
125
126  /**
127   * Load another batch of Id's using a background thread.
128   */
129  protected void loadInBackground(final int requestSize) {
130    if (currentlyBackgroundLoading.get()) {
131      // skip as already background loading
132      logger.debug("... skip background sequence load (another load in progress)");
133      return;
134    }
135    currentlyBackgroundLoading.set(true);
136    backgroundExecutor.execute(() -> {
137      loadMore(requestSize);
138      currentlyBackgroundLoading.set(false);
139    });
140  }
141
142  /**
143   * Read the resultSet returning the list of Id values.
144   */
145  protected abstract List<Long> readIds(ResultSet resultSet, int loadSize) throws SQLException;
146
147  /**
148   * Get more Id's by executing a query and reading the Id's returned.
149   */
150  protected List<Long> getMoreIds(int requestSize) {
151
152    String sql = getSql(requestSize);
153
154    Connection connection = null;
155    PreparedStatement statement = null;
156    ResultSet resultSet = null;
157    try {
158      connection = dataSource.getConnection();
159
160      statement = connection.prepareStatement(sql);
161      resultSet = statement.executeQuery();
162
163      List<Long> newIds = readIds(resultSet, requestSize);
164      if (logger.isTraceEnabled()) {
165        logger.trace("seq:{} loaded:{} sql:{}", seqName, newIds.size(), sql);
166      }
167      if (newIds.isEmpty()) {
168        throw new PersistenceException("Always expecting more than 1 row from " + sql);
169      }
170
171      return newIds;
172
173    } catch (SQLException e) {
174      if (e.getMessage().contains("Database is already closed")) {
175        String msg = "Error getting SEQ when DB shutting down " + e.getMessage();
176        logger.error(msg);
177        System.out.println(msg);
178        return Collections.emptyList();
179      } else {
180        throw new PersistenceException("Error getting sequence nextval", e);
181      }
182    } finally {
183      closeResources(connection, statement, resultSet);
184    }
185  }
186
187  /**
188   * Close the JDBC resources.
189   */
190  private void closeResources(Connection connection, PreparedStatement statement, ResultSet resultSet) {
191    JdbcClose.close(resultSet);
192    JdbcClose.close(statement);
193    JdbcClose.close(connection);
194  }
195
196}