View Javadoc
1   /*
2    * This file is part of Waarp Project (named also Waarp or GG).
3    *
4    *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
5    *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
6    * individual contributors.
7    *
8    *  All Waarp Project is free software: you can redistribute it and/or
9    * modify it under the terms of the GNU General Public License as published by
10   * the Free Software Foundation, either version 3 of the License, or (at your
11   * option) any later version.
12   *
13   * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
14   * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15   * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16   *
17   *  You should have received a copy of the GNU General Public License along with
18   * Waarp . If not, see <http://www.gnu.org/licenses/>.
19   */
20  package org.waarp.common.database;
21  
22  import io.netty.util.Timeout;
23  import io.netty.util.TimerTask;
24  import org.waarp.common.database.exception.WaarpDatabaseNoConnectionException;
25  import org.waarp.common.database.exception.WaarpDatabaseSqlException;
26  import org.waarp.common.database.model.DbModel;
27  import org.waarp.common.database.model.DbModelFactory;
28  import org.waarp.common.guid.GUID;
29  import org.waarp.common.logging.WaarpLogger;
30  import org.waarp.common.logging.WaarpLoggerFactory;
31  import org.waarp.common.lru.ConcurrentUtility;
32  
33  import java.sql.Connection;
34  import java.sql.SQLException;
35  import java.sql.Savepoint;
36  import java.util.ConcurrentModificationException;
37  import java.util.Set;
38  import java.util.concurrent.TimeUnit;
39  import java.util.concurrent.atomic.AtomicInteger;
40  
41  // Notice, do not import com.mysql.jdbc.*
42  // or you will have problems!
43  
44  /**
45   * Class to handle session with the SGBD
46   */
47  public class DbSession {
48    /**
49     * Internal Logger
50     */
51    private static final WaarpLogger logger =
52        WaarpLoggerFactory.getLogger(DbSession.class);
53    private static final String CANNOT_CREATE_CONNECTION =
54        "Cannot create Connection";
55    private static final String THREAD_USING = "ThreadUsing: ";
56  
57    /**
58     * DbAdmin referent object
59     */
60    private DbAdmin admin;
61  
62    /**
63     * The internal connection
64     */
65    private Connection conn;
66  
67    /**
68     * Is this connection Read Only
69     */
70    private boolean isReadOnly = true;
71  
72    /**
73     * Is this session using AutoCommit (true by default)
74     */
75    private boolean autoCommit = true;
76  
77    /**
78     * Internal Id
79     */
80    private GUID internalId;
81  
82    /**
83     * Number of threads using this connection
84     */
85    private final AtomicInteger nbThread = new AtomicInteger(0);
86  
87    /**
88     * To be used when a local Channel is over
89     */
90    private boolean isDisActive = true;
91  
92    /**
93     * List all DbPrepareStatement with long term usage to enable the recreation
94     * when the associated connection is reopened
95     */
96    private final Set<DbPreparedStatement> listPreparedStatement =
97        ConcurrentUtility.newConcurrentSet();
98  
99    private void initialize(final DbModel dbModel, final String server,
100                           final String user, final String passwd,
101                           final boolean isReadOnly, final boolean autoCommit)
102       throws WaarpDatabaseNoConnectionException {
103     if (!DbModelFactory.classLoaded.contains(dbModel.getDbType().name())) {
104       throw new WaarpDatabaseNoConnectionException("DbAdmin not initialzed");
105     }
106     if (server == null) {
107       setConn(null);
108       logger.error("Cannot set a null Server");
109       throw new WaarpDatabaseNoConnectionException("Cannot set a null Server");
110     }
111     try {
112       setAutoCommit(autoCommit);
113       setConn(dbModel.getDbConnection(server, user, passwd));
114       getConn().setAutoCommit(isAutoCommit());
115       setReadOnly(isReadOnly);
116       getConn().setReadOnly(isReadOnly());
117       setInternalId(new GUID());
118       logger.debug("Open Db Conn: {}", getInternalId());
119       DbAdmin.addConnection(getInternalId(), this);
120       setDisActive(false);
121       checkConnection();
122     } catch (final SQLException ex) {
123       setDisActive(true);
124       // handle any errors
125       logger.error(CANNOT_CREATE_CONNECTION + " while already having {}",
126                    DbAdmin.getNbConnection());
127       DbConstant.error(ex);
128       if (getConn() != null) {
129         try {
130           getConn().close();
131         } catch (final SQLException ignored) {
132           // nothing
133         }
134       }
135       setConn(null);
136       throw new WaarpDatabaseNoConnectionException(CANNOT_CREATE_CONNECTION,
137                                                    ex);
138     }
139   }
140 
141   /**
142    * Create a session and connect the current object to the server using the
143    * DbAdmin object. The database access
144    * use auto commit.
145    * <p>
146    * If the initialize is not call before, call it with the default value.
147    *
148    * @param admin
149    * @param isReadOnly
150    *
151    * @throws WaarpDatabaseSqlException
152    */
153   public DbSession(final DbAdmin admin, final boolean isReadOnly)
154       throws WaarpDatabaseNoConnectionException {
155     try {
156       setAdmin(admin);
157       initialize(admin.getDbModel(), admin.getServer(), admin.getUser(),
158                  admin.getPasswd(), isReadOnly, true);
159     } catch (final NullPointerException ex) {
160       // handle any errors
161       setDisActive(true);
162       logger.error(CANNOT_CREATE_CONNECTION + (admin == null), ex);
163       if (getConn() != null) {
164         try {
165           getConn().close();
166         } catch (final SQLException ignored) {
167           // nothing
168         }
169       }
170       setConn(null);
171       throw new WaarpDatabaseNoConnectionException(CANNOT_CREATE_CONNECTION,
172                                                    ex);
173     }
174   }
175 
176   /**
177    * Create a session and connect the current object to the server using the
178    * DbAdmin object.
179    * <p>
180    * If the initialize is not call before, call it with the default value.
181    *
182    * @param admin
183    * @param isReadOnly
184    * @param autoCommit
185    *
186    * @throws WaarpDatabaseSqlException
187    */
188   public DbSession(final DbAdmin admin, final boolean isReadOnly,
189                    final boolean autoCommit)
190       throws WaarpDatabaseNoConnectionException {
191     try {
192       setAdmin(admin);
193       initialize(admin.getDbModel(), admin.getServer(), admin.getUser(),
194                  admin.getPasswd(), isReadOnly, autoCommit);
195     } catch (final NullPointerException ex) {
196       // handle any errors
197       logger.error(CANNOT_CREATE_CONNECTION + (admin == null), ex);
198       setDisActive(true);
199       if (getConn() != null) {
200         try {
201           getConn().close();
202         } catch (final SQLException ignored) {
203           // nothing
204         }
205       }
206       setConn(null);
207       throw new WaarpDatabaseNoConnectionException(CANNOT_CREATE_CONNECTION,
208                                                    ex);
209     }
210   }
211 
212   /**
213    * Change the autocommit feature
214    *
215    * @param autoCommit
216    *
217    * @throws WaarpDatabaseNoConnectionException
218    */
219   public final void setAutoCommit(final boolean autoCommit)
220       throws WaarpDatabaseNoConnectionException {
221     if (getConn() != null) {
222       this.autoCommit = autoCommit;
223       try {
224         getConn().setAutoCommit(autoCommit);
225       } catch (final SQLException e) {
226         // handle any errors
227         logger.error(CANNOT_CREATE_CONNECTION + " while already having {}",
228                      DbAdmin.getNbConnection());
229         DbConstant.error(e);
230         if (getConn() != null) {
231           try {
232             getConn().close();
233           } catch (final SQLException ignored) {
234             // nothing
235           }
236         }
237         setConn(null);
238         setDisActive(true);
239         throw new WaarpDatabaseNoConnectionException(CANNOT_CREATE_CONNECTION,
240                                                      e);
241       }
242     }
243   }
244 
245   /**
246    * @return the admin
247    */
248   public final DbAdmin getAdmin() {
249     return admin;
250   }
251 
252   /**
253    * @param admin the admin to set
254    */
255   protected final void setAdmin(final DbAdmin admin) {
256     this.admin = admin;
257   }
258 
259   /**
260    * To be called when a client will start to use this DbSession (once by
261    * client)
262    */
263   public final void useConnection() {
264     final int val = nbThread.incrementAndGet();
265     synchronized (this) {
266       if (isDisActive()) {
267         try {
268           initialize(getAdmin().getDbModel(), getAdmin().getServer(),
269                      getAdmin().getUser(), getAdmin().getPasswd(), isReadOnly(),
270                      isAutoCommit());
271         } catch (final WaarpDatabaseNoConnectionException e) {
272           logger.error(THREAD_USING + nbThread + " but not connected");
273           return;
274         }
275       }
276     }
277     logger.debug("{}{}", THREAD_USING, val);
278   }
279 
280   /**
281    * To be called when a client will stop to use this DbSession (once by
282    * client)
283    */
284   public final void endUseConnection() {
285     final int val = nbThread.decrementAndGet();
286     logger.debug("{}{}", THREAD_USING, val);
287     if (val <= 0) {
288       disconnect();
289     }
290   }
291 
292   /**
293    * To be called when a client will stop to use this DbSession (once by
294    * client). This version is not blocking.
295    */
296   public final void enUseConnectionNoDisconnect() {
297     final int val = nbThread.decrementAndGet();
298     logger.debug("{}{}", THREAD_USING, val);
299     if (val <= 0) {
300       DbAdmin.dbSessionTimer.newTimeout(new TryDisconnectDbSession(this),
301                                         DbAdmin.WAITFORNETOP * 10,
302                                         TimeUnit.MILLISECONDS);
303     }
304   }
305 
306   /**
307    * To disconnect in asynchronous way the DbSession
308    */
309   private static class TryDisconnectDbSession implements TimerTask {
310     private final DbSession dbSession;
311 
312     private TryDisconnectDbSession(final DbSession dbSession) {
313       this.dbSession = dbSession;
314     }
315 
316     @Override
317     public final void run(final Timeout timeout) {
318       final int val = dbSession.nbThread.get();
319       if (val <= 0) {
320         dbSession.disconnect();
321       }
322       logger.debug("{}{}", THREAD_USING, val);
323     }
324   }
325 
326   @Override
327   public final int hashCode() {
328     return getInternalId().hashCode();
329 
330   }
331 
332   @Override
333   public final boolean equals(final Object o) {
334     if (!(o instanceof DbSession)) {
335       return false;
336     }
337     return this == o || getInternalId().equals(((DbSession) o).getInternalId());
338   }
339 
340   /**
341    * Force the close of the connection
342    */
343   public final void forceDisconnect() {
344     if (getInternalId().equals(getAdmin().getSession().getInternalId())) {
345       logger.debug("Closing internal db connection");
346     }
347     nbThread.set(0);
348     if (getConn() == null) {
349       logger.debug("Connection already closed");
350       return;
351     }
352     logger.debug("DbConnection still in use: {}", nbThread);
353     removeLongTermPreparedStatements();
354     DbAdmin.removeConnection(getInternalId());
355     setDisActive(true);
356     try {
357       logger.debug("Fore close Db Conn: {}", getInternalId());
358       if (getConn() != null) {
359         getConn().close();
360         setConn(null);
361       }
362     } catch (final SQLException e) {
363       logger.warn("Disconnection not OK");
364       DbConstant.error(e);
365     } catch (final ConcurrentModificationException e) {
366       // ignore
367     }
368     logger.info("Current cached connection: {}",
369                 getAdmin().getDbModel().currentNumberOfPooledConnections());
370   }
371 
372   /**
373    * Close the connection
374    */
375   public final void disconnect() {
376     if (getInternalId().equals(getAdmin().getSession().getInternalId())) {
377       logger.debug("Closing internal db connection: {}", nbThread.get());
378     }
379     if (getConn() == null || isDisActive()) {
380       logger.debug("Connection already closed");
381       return;
382     }
383     logger.debug("DbConnection still in use: {}", nbThread);
384     if (nbThread.get() > 0) {
385       logger.info("Still some clients could use this Database Session: {}",
386                   nbThread);
387       return;
388     }
389     synchronized (this) {
390       removeLongTermPreparedStatements();
391       DbAdmin.removeConnection(getInternalId());
392       setDisActive(true);
393       try {
394         logger.debug("Close Db Conn: {}", getInternalId());
395         if (getConn() != null) {
396           getConn().close();
397           setConn(null);
398         }
399       } catch (final SQLException e) {
400         logger.warn("Disconnection not OK");
401         DbConstant.error(e);
402       } catch (final ConcurrentModificationException e) {
403         // ignore
404       }
405     }
406     logger.info("Current cached connection: {}",
407                 getAdmin().getDbModel().currentNumberOfPooledConnections());
408   }
409 
410   /**
411    * Check the connection to the Database and try to reopen it if possible
412    *
413    * @throws WaarpDatabaseNoConnectionException
414    */
415   public final void checkConnection()
416       throws WaarpDatabaseNoConnectionException {
417     try {
418       getAdmin().getDbModel().validConnection(this);
419       setDisActive(false);
420     } catch (final WaarpDatabaseNoConnectionException e) {
421       setDisActive(true);
422       throw e;
423     }
424   }
425 
426   /**
427    * @return True if the connection was successfully reconnected
428    */
429   public final boolean checkConnectionNoException() {
430     try {
431       checkConnection();
432       return true;
433     } catch (final WaarpDatabaseNoConnectionException e) {
434       return false;
435     }
436   }
437 
438   /**
439    * Add a Long Term PreparedStatement
440    *
441    * @param longterm
442    */
443   public final void addLongTermPreparedStatement(
444       final DbPreparedStatement longterm) {
445     listPreparedStatement.add(longterm);
446   }
447 
448   /**
449    * Due to a reconnection, recreate all associated long term
450    * PreparedStatements
451    *
452    * @throws WaarpDatabaseNoConnectionException
453    * @throws WaarpDatabaseSqlException
454    */
455   public final void recreateLongTermPreparedStatements()
456       throws WaarpDatabaseNoConnectionException, WaarpDatabaseSqlException {
457     WaarpDatabaseNoConnectionException elast = null;
458     WaarpDatabaseSqlException e2last = null;
459     logger.info("RecreateLongTermPreparedStatements: {}",
460                 listPreparedStatement.size());
461     for (final DbPreparedStatement longterm : listPreparedStatement) {
462       try {
463         longterm.recreatePreparedStatement();
464       } catch (final WaarpDatabaseNoConnectionException e) {
465         logger.warn(
466             "Error while recreation of Long Term PreparedStatement" + " : {}",
467             e.getMessage());
468         elast = e;
469       } catch (final WaarpDatabaseSqlException e) {
470         logger.warn(
471             "Error while recreation of Long Term PreparedStatement" + " : {}",
472             e.getMessage());
473         e2last = e;
474       }
475     }
476     if (elast != null) {
477       throw elast;
478     }
479     if (e2last != null) {
480       throw e2last;
481     }
482   }
483 
484   /**
485    * Remove all Long Term PreparedStatements (closing connection)
486    */
487   public final void removeLongTermPreparedStatements() {
488     for (final DbPreparedStatement longterm : listPreparedStatement) {
489       if (longterm != null) {
490         longterm.realClose();
491       }
492     }
493     listPreparedStatement.clear();
494   }
495 
496   /**
497    * Remove one Long Term PreparedStatement
498    *
499    * @param longterm
500    */
501   public final void removeLongTermPreparedStatements(
502       final DbPreparedStatement longterm) {
503     listPreparedStatement.remove(longterm);
504   }
505 
506   /**
507    * Commit everything
508    *
509    * @throws WaarpDatabaseSqlException
510    * @throws WaarpDatabaseNoConnectionException
511    */
512   public final void commit()
513       throws WaarpDatabaseSqlException, WaarpDatabaseNoConnectionException {
514     if (getConn() == null) {
515       logger.warn("Cannot commit since connection is null");
516       throw new WaarpDatabaseNoConnectionException(
517           "Cannot commit since connection is null");
518     }
519     if (isAutoCommit()) {
520       return;
521     }
522     if (isDisActive()) {
523       checkConnection();
524     }
525     try {
526       getConn().commit();
527     } catch (final SQLException e) {
528       logger.error("Cannot Commit");
529       DbConstant.error(e);
530       throw new WaarpDatabaseSqlException("Cannot commit", e);
531     }
532   }
533 
534   /**
535    * Rollback from the savepoint or the last set if null
536    *
537    * @param savepoint
538    *
539    * @throws WaarpDatabaseNoConnectionException
540    * @throws WaarpDatabaseSqlException
541    */
542   public final void rollback(final Savepoint savepoint)
543       throws WaarpDatabaseNoConnectionException, WaarpDatabaseSqlException {
544     if (getConn() == null) {
545       logger.warn("Cannot rollback since connection is null");
546       throw new WaarpDatabaseNoConnectionException(
547           "Cannot rollback since connection is null");
548     }
549     if (isDisActive()) {
550       checkConnection();
551     }
552     try {
553       if (savepoint == null) {
554         getConn().rollback();
555       } else {
556         getConn().rollback(savepoint);
557       }
558     } catch (final SQLException e) {
559       logger.error("Cannot rollback");
560       DbConstant.error(e);
561       throw new WaarpDatabaseSqlException("Cannot rollback", e);
562     }
563   }
564 
565   /**
566    * Make a savepoint
567    *
568    * @return the new savepoint
569    *
570    * @throws WaarpDatabaseNoConnectionException
571    * @throws WaarpDatabaseSqlException
572    */
573   public final Savepoint savepoint()
574       throws WaarpDatabaseNoConnectionException, WaarpDatabaseSqlException {
575     if (getConn() == null) {
576       logger.warn("Cannot savepoint since connection is null");
577       throw new WaarpDatabaseNoConnectionException(
578           "Cannot savepoint since connection is null");
579     }
580     if (isDisActive()) {
581       checkConnection();
582     }
583     try {
584       return getConn().setSavepoint();
585     } catch (final SQLException e) {
586       logger.error("Cannot savepoint");
587       DbConstant.error(e);
588       throw new WaarpDatabaseSqlException("Cannot savepoint", e);
589     }
590   }
591 
592   /**
593    * Release the savepoint
594    *
595    * @param savepoint
596    *
597    * @throws WaarpDatabaseNoConnectionException
598    * @throws WaarpDatabaseSqlException
599    */
600   public final void releaseSavepoint(final Savepoint savepoint)
601       throws WaarpDatabaseNoConnectionException, WaarpDatabaseSqlException {
602     if (getConn() == null) {
603       logger.warn("Cannot release savepoint since connection is null");
604       throw new WaarpDatabaseNoConnectionException(
605           "Cannot release savepoint since connection is null");
606     }
607     if (isDisActive()) {
608       checkConnection();
609     }
610     try {
611       getConn().releaseSavepoint(savepoint);
612     } catch (final SQLException e) {
613       logger.error("Cannot release savepoint");
614       DbConstant.error(e);
615       throw new WaarpDatabaseSqlException("Cannot release savepoint", e);
616     }
617   }
618 
619   /**
620    * @return the isReadOnly
621    */
622   public final boolean isReadOnly() {
623     return isReadOnly;
624   }
625 
626   /**
627    * @param isReadOnly the isReadOnly to set
628    */
629   public final void setReadOnly(final boolean isReadOnly) {
630     this.isReadOnly = isReadOnly;
631   }
632 
633   /**
634    * @return the autoCommit
635    */
636   public final boolean isAutoCommit() {
637     return autoCommit;
638   }
639 
640   /**
641    * @return the conn
642    */
643   public final Connection getConn() {
644     return conn;
645   }
646 
647   /**
648    * @param conn the conn to set
649    */
650   public final void setConn(final Connection conn) {
651     this.conn = conn;
652   }
653 
654   /**
655    * @return the internalId
656    */
657   public final GUID getInternalId() {
658     return internalId;
659   }
660 
661   /**
662    * @param internalId the internalId to set
663    */
664   private void setInternalId(final GUID internalId) {
665     this.internalId = internalId;
666   }
667 
668   /**
669    * @return the isDisActive
670    */
671   public final boolean isDisActive() {
672     return isDisActive;
673   }
674 
675   /**
676    * @param isDisActive the isDisActive to set
677    */
678   public final void setDisActive(final boolean isDisActive) {
679     this.isDisActive = isDisActive;
680   }
681 }