1 package org.apache.torque.oid;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 import java.math.BigDecimal;
20 import java.sql.Connection;
21 import java.sql.ResultSet;
22 import java.sql.Statement;
23 import java.util.ArrayList;
24 import java.util.Hashtable;
25 import java.util.Iterator;
26 import java.util.List;
27
28 import org.apache.commons.configuration.Configuration;
29
30 import org.apache.commons.logging.Log;
31 import org.apache.commons.logging.LogFactory;
32
33 import org.apache.torque.Torque;
34 import org.apache.torque.TorqueException;
35 import org.apache.torque.map.DatabaseMap;
36 import org.apache.torque.map.TableMap;
37 import org.apache.torque.util.Transaction;
38
39
40
41
42
43
44 /***
45 * This method of ID generation is used to ensure that code is
46 * more database independent. For example, MySQL has an auto-increment
47 * feature while Oracle uses sequences. It caches several ids to
48 * avoid needing a Connection for every request.
49 *
50 * This class uses the table ID_TABLE defined in
51 * conf/master/id-table-schema.xml. The columns in ID_TABLE are used as
52 * follows:<br>
53 *
54 * ID_TABLE_ID - The PK for this row (any unique int).<br>
55 * TABLE_NAME - The name of the table you want ids for.<br>
56 * NEXT_ID - The next id returned by IDBroker when it queries the
57 * database (not when it returns an id from memory).<br>
58 * QUANTITY - The number of ids that IDBroker will cache in memory.<br>
59 * <p>
60 * Use this class like this:
61 * <pre>
62 * int id = dbMap.getIDBroker().getNextIdAsInt(null, "TABLE_NAME");
63 * - or -
64 * BigDecimal[] ids = ((IDBroker)dbMap.getIDBroker())
65 * .getNextIds("TABLE_NAME", numOfIdsToReturn);
66 * </pre>
67 *
68 * NOTE: When the ID_TABLE must be updated we must ensure that
69 * IDBroker objects running in different JVMs do not overwrite each
70 * other. This is accomplished using using the transactional support
71 * occuring in some databases. Using this class with a database that
72 * does not support transactions should be limited to a single JVM.
73 *
74 * @author <a href="mailto:frank.kim@clearink.com">Frank Y. Kim</a>
75 * @author <a href="mailto:jmcnally@collab.net">John D. McNally</a>
76 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
77 * @version $Id: IDBroker.java 239630 2005-08-24 12:25:32Z henning $
78 */
79 public class IDBroker implements Runnable, IdGenerator
80 {
81 /*** Name of the ID_TABLE = ID_TABLE */
82 public static final String ID_TABLE = "ID_TABLE";
83
84 /*** Table_Name column name */
85 public static final String COL_TABLE_NAME = "TABLE_NAME";
86
87 /*** Fully qualified Table_Name column name */
88 public static final String TABLE_NAME = ID_TABLE + "." + COL_TABLE_NAME;
89
90 /*** ID column name */
91 public static final String COL_TABLE_ID = "ID_TABLE_ID";
92
93 /*** Fully qualified ID column name */
94 public static final String TABLE_ID = ID_TABLE + "." + COL_TABLE_ID;
95
96 /*** Next_ID column name */
97 public static final String COL_NEXT_ID = "NEXT_ID";
98
99 /*** Fully qualified Next_ID column name */
100 public static final String NEXT_ID = ID_TABLE + "." + COL_NEXT_ID;
101
102 /*** Quantity column name */
103 public static final String COL_QUANTITY = "QUANTITY";
104
105 /*** Fully qualified Quantity column name */
106 public static final String QUANTITY = ID_TABLE + "." + COL_QUANTITY;
107
108 /*** The TableMap referencing the ID_TABLE for this IDBroker. */
109 private TableMap tableMap;
110
111 /***
112 * The default size of the per-table meta data <code>Hashtable</code>
113 * objects.
114 */
115 private static final int DEFAULT_SIZE = 40;
116
117 /***
118 * The cached IDs for each table.
119 *
120 * Key: String table name.
121 * Value: List of Integer IDs.
122 */
123 private Hashtable ids = new Hashtable(DEFAULT_SIZE);
124
125 /***
126 * The quantity of ids to grab for each table.
127 *
128 * Key: String table name.
129 * Value: Integer quantity.
130 */
131 private Hashtable quantityStore = new Hashtable(DEFAULT_SIZE);
132
133 /***
134 * The last time this IDBroker queried the database for ids.
135 *
136 * Key: String table name.
137 * Value: Date of last id request.
138 */
139 private Hashtable lastQueryTime = new Hashtable(DEFAULT_SIZE);
140
141 /***
142 * Amount of time for the thread to sleep
143 */
144 private static final int SLEEP_PERIOD = 1 * 60000;
145
146 /***
147 * The safety Margin
148 */
149 private static final float SAFETY_MARGIN = 1.2f;
150
151 /***
152 * The houseKeeperThread thread
153 */
154 private Thread houseKeeperThread = null;
155
156 /***
157 * Are transactions supported?
158 */
159 private boolean transactionsSupported = false;
160
161 /***
162 * The value of ONE!
163 */
164 private static final BigDecimal ONE = new BigDecimal("1");
165
166 /*** the configuration */
167 private Configuration configuration;
168
169 /*** property name */
170 private static final String DB_IDBROKER_CLEVERQUANTITY =
171 "idbroker.clever.quantity";
172
173 /*** property name */
174 private static final String DB_IDBROKER_PREFETCH =
175 "idbroker.prefetch";
176
177 /*** property name */
178 private static final String DB_IDBROKER_USENEWCONNECTION =
179 "idbroker.usenewconnection";
180
181 /*** the log */
182 private Log log = LogFactory.getLog(IDBroker.class);
183
184 /***
185 * Creates an IDBroker for the ID table.
186 *
187 * @param tMap A TableMap.
188 */
189 public IDBroker(TableMap tMap)
190 {
191 this.tableMap = tMap;
192 configuration = Torque.getConfiguration();
193
194
195 if (configuration.getBoolean(DB_IDBROKER_PREFETCH, true))
196 {
197 houseKeeperThread = new Thread(this);
198
199
200
201
202 houseKeeperThread.setDaemon(true);
203 houseKeeperThread.setName("Torque - ID Broker thread");
204 houseKeeperThread.start();
205 }
206
207
208
209
210 String dbName = tMap.getDatabaseMap().getName();
211 Connection dbCon = null;
212 try
213 {
214 dbCon = Torque.getConnection(dbName);
215 transactionsSupported = dbCon.getMetaData().supportsTransactions();
216 }
217 catch (Exception e)
218 {
219 transactionsSupported = false;
220 }
221 finally
222 {
223 try
224 {
225
226 dbCon.close();
227 }
228 catch (Exception e)
229 {
230 }
231 }
232 if (!transactionsSupported)
233 {
234 log.warn("IDBroker is being used with db '" + dbName
235 + "', which does not support transactions. IDBroker "
236 + "attempts to use transactions to limit the possibility "
237 + "of duplicate key generation. Without transactions, "
238 + "duplicate key generation is possible if multiple JVMs "
239 + "are used or other means are used to write to the "
240 + "database.");
241 }
242 }
243
244 /***
245 * Set the configuration
246 *
247 * @param configuration the configuration
248 */
249 public void setConfiguration(Configuration configuration)
250 {
251 this.configuration = configuration;
252 }
253
254 /***
255 * Returns an id as a primitive int. Note this method does not
256 * require a Connection, it just implements the KeyGenerator
257 * interface. if a Connection is needed one will be requested.
258 * To force the use of the passed in connection set the configuration
259 * property torque.idbroker.usenewconnection = false
260 *
261 * @param connection A Connection.
262 * @param tableName an Object that contains additional info.
263 * @return An int with the value for the id.
264 * @exception Exception Database error.
265 */
266 public int getIdAsInt(Connection connection, Object tableName)
267 throws Exception
268 {
269 return getIdAsBigDecimal(connection, tableName).intValue();
270 }
271
272
273 /***
274 * Returns an id as a primitive long. Note this method does not
275 * require a Connection, it just implements the KeyGenerator
276 * interface. if a Connection is needed one will be requested.
277 * To force the use of the passed in connection set the configuration
278 * property torque.idbroker.usenewconnection = false
279 *
280 * @param connection A Connection.
281 * @param tableName a String that identifies a table.
282 * @return A long with the value for the id.
283 * @exception Exception Database error.
284 */
285 public long getIdAsLong(Connection connection, Object tableName)
286 throws Exception
287 {
288 return getIdAsBigDecimal(connection, tableName).longValue();
289 }
290
291 /***
292 * Returns an id as a BigDecimal. Note this method does not
293 * require a Connection, it just implements the KeyGenerator
294 * interface. if a Connection is needed one will be requested.
295 * To force the use of the passed in connection set the configuration
296 * property torque.idbroker.usenewconnection = false
297 *
298 * @param connection A Connection.
299 * @param tableName a String that identifies a table..
300 * @return A BigDecimal id.
301 * @exception Exception Database error.
302 */
303 public BigDecimal getIdAsBigDecimal(Connection connection,
304 Object tableName)
305 throws Exception
306 {
307 BigDecimal[] id = getNextIds((String) tableName, 1, connection);
308 return id[0];
309 }
310
311 /***
312 * Returns an id as a String. Note this method does not
313 * require a Connection, it just implements the KeyGenerator
314 * interface. if a Connection is needed one will be requested.
315 * To force the use of the passed in connection set the configuration
316 * property torque.idbroker.usenewconnection = false
317 *
318 * @param connection A Connection should be null.
319 * @param tableName a String that identifies a table.
320 * @return A String id
321 * @exception Exception Database error.
322 */
323 public String getIdAsString(Connection connection, Object tableName)
324 throws Exception
325 {
326 return getIdAsBigDecimal(connection, tableName).toString();
327 }
328
329
330 /***
331 * A flag to determine the timing of the id generation *
332 * @return a <code>boolean</code> value
333 */
334 public boolean isPriorToInsert()
335 {
336 return true;
337 }
338
339 /***
340 * A flag to determine the timing of the id generation
341 *
342 * @return a <code>boolean</code> value
343 */
344 public boolean isPostInsert()
345 {
346 return false;
347 }
348
349 /***
350 * A flag to determine whether a Connection is required to
351 * generate an id.
352 *
353 * @return a <code>boolean</code> value
354 */
355 public boolean isConnectionRequired()
356 {
357 return false;
358 }
359
360 /***
361 * This method returns x number of ids for the given table.
362 *
363 * @param tableName The name of the table for which we want an id.
364 * @param numOfIdsToReturn The desired number of ids.
365 * @return A BigDecimal.
366 * @exception Exception Database error.
367 */
368 public synchronized BigDecimal[] getNextIds(String tableName,
369 int numOfIdsToReturn)
370 throws Exception
371 {
372 return getNextIds(tableName, numOfIdsToReturn, null);
373 }
374
375 /***
376 * This method returns x number of ids for the given table.
377 * Note this method does not require a Connection.
378 * If a Connection is needed one will be requested.
379 * To force the use of the passed in connection set the configuration
380 * property torque.idbroker.usenewconnection = false
381 *
382 * @param tableName The name of the table for which we want an id.
383 * @param numOfIdsToReturn The desired number of ids.
384 * @param connection A Connection.
385 * @return A BigDecimal.
386 * @exception Exception Database error.
387 */
388 public synchronized BigDecimal[] getNextIds(String tableName,
389 int numOfIdsToReturn,
390 Connection connection)
391 throws Exception
392 {
393 if (tableName == null)
394 {
395 throw new Exception("getNextIds(): tableName == null");
396 }
397
398
399
400
401
402
403
404
405
406
407 List availableIds = (List) ids.get(tableName);
408
409 if (availableIds == null || availableIds.size() < numOfIdsToReturn)
410 {
411 if (availableIds == null)
412 {
413 log.debug("Forced id retrieval - no available list");
414 }
415 else
416 {
417 log.debug("Forced id retrieval - " + availableIds.size());
418 }
419 storeIDs(tableName, true, connection);
420 availableIds = (List) ids.get(tableName);
421 }
422
423 int size = availableIds.size() < numOfIdsToReturn
424 ? availableIds.size() : numOfIdsToReturn;
425
426 BigDecimal[] results = new BigDecimal[size];
427
428
429
430
431
432
433 for (int i = size - 1; i >= 0; i--)
434 {
435 results[i] = (BigDecimal) availableIds.get(i);
436 availableIds.remove(i);
437 }
438
439
440 return results;
441 }
442
443 /***
444 * Describe <code>exists</code> method here.
445 *
446 * @param tableName a <code>String</code> value that is used to identify
447 * the row
448 * @return a <code>boolean</code> value
449 * @exception TorqueException if an error occurs
450 * @exception Exception a generic exception.
451 */
452 public boolean exists(String tableName)
453 throws TorqueException, Exception
454 {
455 String query = new StringBuffer(100)
456 .append("select ")
457 .append(TABLE_NAME)
458 .append(" where ")
459 .append(TABLE_NAME).append("='").append(tableName).append('\'')
460 .toString();
461
462 boolean exists = false;
463 Connection dbCon = null;
464 try
465 {
466 String databaseName = tableMap.getDatabaseMap().getName();
467
468 dbCon = Torque.getConnection(databaseName);
469 Statement statement = dbCon.createStatement();
470 ResultSet rs = statement.executeQuery(query);
471 exists = rs.next();
472 statement.close();
473 }
474 finally
475 {
476
477 try
478 {
479 dbCon.close();
480 }
481 catch (Exception e)
482 {
483 log.error("Release of connection failed.", e);
484 }
485 }
486 return exists;
487 }
488
489 /***
490 * A background thread that tries to ensure that when someone asks
491 * for ids, that there are already some loaded and that the
492 * database is not accessed.
493 */
494 public void run()
495 {
496 log.debug("IDBroker thread was started.");
497
498 Thread thisThread = Thread.currentThread();
499 while (houseKeeperThread == thisThread)
500 {
501 try
502 {
503 Thread.sleep(SLEEP_PERIOD);
504 }
505 catch (InterruptedException exc)
506 {
507
508 }
509
510
511 Iterator it = ids.keySet().iterator();
512 while (it.hasNext())
513 {
514 String tableName = (String) it.next();
515 if (log.isDebugEnabled())
516 {
517 log.debug("IDBroker thread checking for more keys "
518 + "on table: " + tableName);
519 }
520 List availableIds = (List) ids.get(tableName);
521 int quantity = getQuantity(tableName, null).intValue();
522 if (quantity > availableIds.size())
523 {
524 try
525 {
526
527
528
529 storeIDs(tableName, false, null);
530 if (log.isDebugEnabled())
531 {
532 log.debug("Retrieved more ids for table: " + tableName);
533 }
534 }
535 catch (Exception exc)
536 {
537 log.error("There was a problem getting new IDs "
538 + "for table: " + tableName, exc);
539 }
540 }
541 }
542 }
543 log.debug("IDBroker thread finished.");
544 }
545
546 /***
547 * Shuts down the IDBroker thread.
548 *
549 * Calling this method stops the thread that was started for this
550 * instance of the IDBroker. This method should be called during
551 * MapBroker Service shutdown.
552 */
553 public void stop()
554 {
555 houseKeeperThread = null;
556 }
557
558 /***
559 * Check the frequency of retrieving new ids from the database.
560 * If the frequency is high then we increase the amount (i.e.
561 * quantity column) of ids retrieved on each access. Tries to
562 * alter number of keys grabbed so that IDBroker retrieves a new
563 * set of ID's prior to their being needed.
564 *
565 * @param tableName The name of the table for which we want an id.
566 */
567 private void checkTiming(String tableName)
568 {
569
570
571 if (!configuration.getBoolean(DB_IDBROKER_CLEVERQUANTITY, true)
572 || !configuration.getBoolean(DB_IDBROKER_PREFETCH, true))
573 {
574 return;
575 }
576
577
578 java.util.Date lastTime = (java.util.Date) lastQueryTime.get(tableName);
579 java.util.Date now = new java.util.Date();
580
581 if (lastTime != null)
582 {
583 long thenLong = lastTime.getTime();
584 long nowLong = now.getTime();
585 int timeLapse = (int) (nowLong - thenLong);
586 if (timeLapse < SLEEP_PERIOD && timeLapse > 0)
587 {
588 if (log.isDebugEnabled())
589 {
590 log.debug("Unscheduled retrieval of more ids for table: "
591 + tableName);
592 }
593
594
595 float rate = getQuantity(tableName, null).floatValue()
596 / (float) timeLapse;
597 quantityStore.put(tableName, new BigDecimal(
598 Math.ceil(SLEEP_PERIOD * rate * SAFETY_MARGIN)));
599 }
600 }
601 lastQueryTime.put(tableName, now);
602 }
603
604 /***
605 * Grabs more ids from the id_table and stores it in the ids
606 * Hashtable. If adjustQuantity is set to true the amount of id's
607 * retrieved for each call to storeIDs will be adjusted.
608 *
609 * @param tableName The name of the table for which we want an id.
610 * @param adjustQuantity True if amount should be adjusted.
611 * @param connection a Connection
612 * @exception Exception a generic exception.
613 */
614 private synchronized void storeIDs(String tableName,
615 boolean adjustQuantity,
616 Connection connection)
617 throws Exception
618 {
619 BigDecimal nextId = null;
620 BigDecimal quantity = null;
621 DatabaseMap dbMap = tableMap.getDatabaseMap();
622
623
624
625
626
627
628 if (adjustQuantity)
629 {
630 checkTiming(tableName);
631 }
632
633 boolean useNewConnection = (connection == null) || (configuration
634 .getBoolean(DB_IDBROKER_USENEWCONNECTION, true));
635 try
636 {
637 if (useNewConnection)
638 {
639 connection = Transaction.beginOptional(dbMap.getName(),
640 transactionsSupported);
641 }
642
643
644
645
646
647
648 quantity = getQuantity(tableName, connection);
649 updateQuantity(connection, tableName, quantity);
650
651
652 BigDecimal[] results = selectRow(connection, tableName);
653 nextId = results[0];
654
655
656
657 BigDecimal newNextId = nextId.add(quantity);
658 updateNextId(connection, tableName, newNextId.toString());
659
660 if (useNewConnection)
661 {
662 Transaction.commit(connection);
663 }
664 }
665 catch (Exception e)
666 {
667 if (useNewConnection)
668 {
669 Transaction.rollback(connection);
670 }
671 throw e;
672 }
673
674 List availableIds = (List) ids.get(tableName);
675 if (availableIds == null)
676 {
677 availableIds = new ArrayList();
678 ids.put(tableName, availableIds);
679 }
680
681
682 int numId = quantity.intValue();
683 for (int i = 0; i < numId; i++)
684 {
685 availableIds.add(nextId);
686 nextId = nextId.add(ONE);
687 }
688
689 }
690
691 /***
692 * This method allows you to get the number of ids that are to be
693 * cached in memory. This is either stored in quantityStore or
694 * read from the db. (ie the value in ID_TABLE.QUANTITY).
695 *
696 * Though this method returns a BigDecimal for the quantity, it is
697 * unlikey the system could withstand whatever conditions would lead
698 * to really needing a large quantity, it is retrieved as a BigDecimal
699 * only because it is going to be added to another BigDecimal.
700 *
701 * @param tableName The name of the table we want to query.
702 * @param connection a Connection
703 * @return An int with the number of ids cached in memory.
704 */
705 private BigDecimal getQuantity(String tableName, Connection connection)
706 {
707 BigDecimal quantity = null;
708
709
710 if (!configuration.getBoolean(DB_IDBROKER_PREFETCH, true))
711 {
712 quantity = new BigDecimal(1);
713 }
714
715 else if (quantityStore.containsKey(tableName))
716 {
717 quantity = (BigDecimal) quantityStore.get(tableName);
718 }
719 else
720 {
721 Connection dbCon = null;
722 try
723 {
724 if (connection == null || configuration
725 .getBoolean(DB_IDBROKER_USENEWCONNECTION, true))
726 {
727 String databaseName = tableMap.getDatabaseMap().getName();
728
729 dbCon = Torque.getConnection(databaseName);
730 }
731
732
733 BigDecimal[] results = selectRow(dbCon, tableName);
734
735
736 quantity = results[1];
737 quantityStore.put(tableName, quantity);
738 }
739 catch (Exception e)
740 {
741 quantity = new BigDecimal(10);
742 }
743 finally
744 {
745
746 try
747 {
748 dbCon.close();
749 }
750 catch (Exception e)
751 {
752 log.error("Release of connection failed.", e);
753 }
754 }
755 }
756 return quantity;
757 }
758
759 /***
760 * Helper method to select a row in the ID_TABLE.
761 *
762 * @param con A Connection.
763 * @param tableName The properly escaped name of the table to
764 * identify the row.
765 * @return A BigDecimal[].
766 * @exception Exception a generic exception.
767 */
768 private BigDecimal[] selectRow(Connection con, String tableName)
769 throws Exception
770 {
771 StringBuffer stmt = new StringBuffer();
772 stmt.append("SELECT ")
773 .append(COL_NEXT_ID)
774 .append(", ")
775 .append(COL_QUANTITY)
776 .append(" FROM ")
777 .append(ID_TABLE)
778 .append(" WHERE ")
779 .append(COL_TABLE_NAME)
780 .append(" = '")
781 .append(tableName)
782 .append('\'');
783
784 Statement statement = null;
785
786 BigDecimal[] results = new BigDecimal[2];
787 try
788 {
789 statement = con.createStatement();
790 ResultSet rs = statement.executeQuery(stmt.toString());
791
792 if (rs.next())
793 {
794
795
796
797 results[0] = new BigDecimal(rs.getString(1));
798 results[1] = new BigDecimal(rs.getString(2));
799 }
800 else
801 {
802 throw new TorqueException("The table " + tableName
803 + " does not have a proper entry in the " + ID_TABLE);
804 }
805 }
806 finally
807 {
808 if (statement != null)
809 {
810 statement.close();
811 }
812 }
813
814 return results;
815 }
816
817 /***
818 * Helper method to update a row in the ID_TABLE.
819 *
820 * @param con A Connection.
821 * @param tableName The properly escaped name of the table to identify the
822 * row.
823 * @param id An int with the value to set for the id.
824 * @exception Exception Database error.
825 */
826 private void updateNextId(Connection con, String tableName, String id)
827 throws Exception
828 {
829
830
831 StringBuffer stmt = new StringBuffer(id.length()
832 + tableName.length() + 50);
833 stmt.append("UPDATE " + ID_TABLE)
834 .append(" SET ")
835 .append(COL_NEXT_ID)
836 .append(" = ")
837 .append(id)
838 .append(" WHERE ")
839 .append(COL_TABLE_NAME)
840 .append(" = '")
841 .append(tableName)
842 .append('\'');
843
844 Statement statement = null;
845
846 if (log.isDebugEnabled())
847 {
848 log.debug("updateNextId: " + stmt.toString());
849 }
850
851 try
852 {
853 statement = con.createStatement();
854 statement.executeUpdate(stmt.toString());
855 }
856 finally
857 {
858 if (statement != null)
859 {
860 statement.close();
861 }
862 }
863 }
864
865 /***
866 * Helper method to update a row in the ID_TABLE.
867 *
868 * @param con A Connection.
869 * @param tableName The properly escaped name of the table to identify the
870 * row.
871 * @param quantity An int with the value of the quantity.
872 * @exception Exception Database error.
873 */
874 private void updateQuantity(Connection con, String tableName,
875 BigDecimal quantity)
876 throws Exception
877 {
878 StringBuffer stmt = new StringBuffer(quantity.toString().length()
879 + tableName.length() + 50);
880 stmt.append("UPDATE ")
881 .append(ID_TABLE)
882 .append(" SET ")
883 .append(COL_QUANTITY)
884 .append(" = ")
885 .append(quantity)
886 .append(" WHERE ")
887 .append(COL_TABLE_NAME)
888 .append(" = '")
889 .append(tableName)
890 .append('\'');
891
892 Statement statement = null;
893
894 if (log.isDebugEnabled())
895 {
896 log.debug("updateQuantity: " + stmt.toString());
897 }
898
899 try
900 {
901 statement = con.createStatement();
902 statement.executeUpdate(stmt.toString());
903 }
904 finally
905 {
906 if (statement != null)
907 {
908 statement.close();
909 }
910 }
911 }
912 }