001package com.avaje.ebean.config.dbplatform;
002
003import java.sql.Connection;
004import java.sql.PreparedStatement;
005import java.sql.ResultSet;
006import java.sql.SQLException;
007import java.util.ArrayList;
008
009import javax.persistence.PersistenceException;
010import javax.sql.DataSource;
011
012import com.avaje.ebean.BackgroundExecutor;
013import com.avaje.ebean.Transaction;
014import org.slf4j.Logger;
015import org.slf4j.LoggerFactory;
016
017/**
018 * Database sequence based IdGenerator.
019 */
020public abstract class SequenceIdGenerator implements PlatformIdGenerator {
021
022  private static final Logger logger = LoggerFactory.getLogger(SequenceIdGenerator.class);
023
024  /**
025   * Used to synchronise the idList access.
026   */
027  protected final Object monitor = new Object();
028
029  /**
030   * Used to synchronise background loading (loadBatchInBackground).
031   */
032  protected final Object backgroundLoadMonitor = new Object();
033
034  /**
035   * The actual sequence name.
036   */
037  protected final String seqName;
038
039  protected final DataSource dataSource;
040
041  protected final BackgroundExecutor backgroundExecutor;
042
043  protected final ArrayList<Long> idList = new ArrayList<Long>(50);
044
045  protected final int batchSize;
046
047  protected int currentlyBackgroundLoading;
048
049  /**
050   * Construct given a dataSource and sql to return the next sequence value.
051   */
052  public SequenceIdGenerator(BackgroundExecutor be, DataSource ds, String seqName, int batchSize) {
053    this.backgroundExecutor = be;
054    this.dataSource = ds;
055    this.seqName = seqName;
056    this.batchSize = batchSize;
057  }
058
059  public abstract String getSql(int batchSize);
060
061  /**
062   * Returns the sequence name.
063   */
064  public String getName() {
065    return seqName;
066  }
067
068  /**
069   * Returns true.
070   */
071  public boolean isDbSequence() {
072    return true;
073  }
074
075  /**
076   * If allocateSize is large load some sequences in a background thread.
077   * <p>
078   * For example, when inserting a bean with a cascade on a OneToMany with many
079   * beans Ebean can call this to ensure .
080   * </p>
081   */
082  public void preAllocateIds(int allocateSize) {
083    if (batchSize > 1 && allocateSize > batchSize) {
084      // only bother if allocateSize is bigger than
085      // the normal loading batchSize
086      if (allocateSize > 100) {
087        // max out at 100 for now
088        allocateSize = 100;
089      }
090      loadLargeAllocation(allocateSize);
091    }
092  }
093
094  /**
095   * Called by preAllocateIds when we know that a large number of Id's is going
096   * to be needed shortly.
097   */
098  protected void loadLargeAllocation(final int allocateSize) {
099    // preAllocateIds was called with a relatively large batchSize
100    // so we will just go ahead and load those anyway in background
101    backgroundExecutor.execute(new Runnable() {
102      public void run() {
103        loadMoreIds(allocateSize, null);
104      }
105    });
106  }
107
108  /**
109   * Return the next Id.
110   * <p>
111   * If a Transaction has been passed in use the Connection from it.
112   * </p>
113   */
114  public Object nextId(Transaction t) {
115    synchronized (monitor) {
116
117      if (idList.isEmpty()) {
118        loadMoreIds(batchSize, t);
119      }
120      Long nextId = idList.remove(0);
121
122      if (batchSize > 1) {
123        if (idList.size() <= batchSize / 2) {
124          loadBatchInBackground();
125        }
126      }
127
128      return nextId;
129    }
130  }
131
132  /**
133   * Load another batch of Id's using a background thread.
134   */
135  protected void loadBatchInBackground() {
136
137    // single threaded processing...
138    synchronized (backgroundLoadMonitor) {
139
140      if (currentlyBackgroundLoading > 0) {
141        // skip as already background loading
142        logger.debug("... skip background sequence load (another load in progress)");
143        return;
144      }
145
146      currentlyBackgroundLoading = batchSize;
147
148      backgroundExecutor.execute(new Runnable() {
149        public void run() {
150          loadMoreIds(batchSize, null);
151          synchronized (backgroundLoadMonitor) {
152            currentlyBackgroundLoading = 0;
153          }
154        }
155      });
156    }
157  }
158
159  protected void loadMoreIds(final int numberToLoad, Transaction t) {
160
161    ArrayList<Long> newIds = getMoreIds(numberToLoad, t);
162
163    if (logger.isDebugEnabled()) {
164      logger.debug("... seq:" + seqName + " loaded:" + numberToLoad + " ids:" + newIds);
165    }
166
167    synchronized (monitor) {
168      for (int i = 0; i < newIds.size(); i++) {
169        idList.add(newIds.get(i));
170      }
171    }
172  }
173
174  /**
175   * Get more Id's by executing a query and reading the Id's returned.
176   */
177  protected ArrayList<Long> getMoreIds(int loadSize, Transaction t) {
178
179    String sql = getSql(loadSize);
180
181    ArrayList<Long> newIds = new ArrayList<Long>(loadSize);
182
183    boolean useTxnConnection = t != null;
184
185    Connection c = null;
186    PreparedStatement pstmt = null;
187    ResultSet rset = null;
188    try {
189      c = useTxnConnection ? t.getConnection() : dataSource.getConnection();
190
191      pstmt = c.prepareStatement(sql);
192      rset = pstmt.executeQuery();
193      while (rset.next()) {
194        newIds.add(rset.getLong(1));
195      }
196      if (newIds.isEmpty()) {
197        throw new PersistenceException("Always expecting more than 1 row from " + sql);
198      }
199
200      return newIds;
201
202    } catch (SQLException e) {
203      if (e.getMessage().contains("Database is already closed")) {
204        String msg = "Error getting SEQ when DB shutting down " + e.getMessage();
205        logger.info(msg);
206        System.out.println(msg);
207        return newIds;
208      } else {
209        throw new PersistenceException("Error getting sequence nextval", e);
210      }
211    } finally {
212      if (useTxnConnection) {
213        closeResources(null, pstmt, rset);
214      } else {
215        closeResources(c, pstmt, rset);
216      }
217    }
218  }
219
220  /**
221   * Close the JDBC resources.
222   */
223  protected void closeResources(Connection c, PreparedStatement pstmt, ResultSet rset) {
224    try {
225      if (rset != null) {
226        rset.close();
227      }
228    } catch (SQLException e) {
229      logger.error( "Error closing ResultSet", e);
230    }
231    try {
232      if (pstmt != null) {
233        pstmt.close();
234      }
235    } catch (SQLException e) {
236      logger.error("Error closing PreparedStatement", e);
237    }
238    try {
239      if (c != null) {
240        c.close();
241      }
242    } catch (SQLException e) {
243      logger.error("Error closing Connection", e);
244    }
245  }
246
247}