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