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