MokaByte 53 - Giugno 2001
Foto dell'autore non disponibile
di
Giovanni Tommasi
Object Pooling Parte II
Esempi concreti: i Connection Pool
Il mese scorso abbiamo trattato l’argomento dei pool di oggetti dal punto di vista teorico, cercando di identificare le caratteristiche salienti ed i punti critici di questo strumento. Ora, come promesso, cercheremo di vedere a cosa serve nella realtà un pool di oggetti, studiando un caso concreto che riguarda un pool di connessioni usato nell’ambito di una servlet

Il problema delle connessioni nelle applicazioni web è uno di quelli che richiedono soluzioni di alto livello, sia dal punto di vista della progettazione che della realizzazione.
Supponiamo di avere una servlet che interroga un database, e vediamo dove e come si può affrontare il problema della connessione. Ci sono sicuramente due possibilità:
  • l’oggetto java.sql.Connection viene dichiarato come attributo e poi “istanziato” nell’init(), oppure la prima volta che si entra nel service(), e poi chiuso nel destroy().
  • L’oggetto java.sql.Connection viene dichiarato e istanziato nel service() prima di effettuare le operazioni sql sul db, e poi chiuso, sempre nel service(), alla fine di queste.


Ambedue queste possibilità hanno dei pro e dei contro. Nel primo caso, la connessione viene aperta e chiusa una volta sola, e questo comporta senz’altro un’ottimizzazione delle prestazioni, poichè l’apertura di una connessione è una cosa ‘pesante’, soprattutto su web. D’altro canto, però, la nostra servlet dovrà implementare SingleThreadModel, oppure il service() dovrà essere dichiarato synchronized. Questo perchè Connection, se dichiarata come attributo della classe, è una risorsa condivisa da tutti i thread che vengono lanciati, e quindi bisogna impedire che più thread tentino di usarla contemporaneamente (appunto impedendo che vengano lanciati più thread). Quindi saremo in grado di evadere una richiesta per volta, e questo provocherà un peggioramento delle prestazioni, in quanto le richieste verranno serializzate (accodate). Nel secondo caso, invece, non abbiamo alcun problema di accesso concorrente, perchè la dichiarazione di Connection sta dentro il metodo service(), per cui vengono create tante Connection quanti sono i thread lanciati. Tuttavia, la connessione viene aperta e chiusa, da parte di ogni utente, tutte le volte che si effettua una richiesta, e anche questo è un carico di lavoro molto pesante per il server.
L’uso di un pool di connessioni consente di sfruttare la parte migliore di ambedue queste tecniche senza doverne sopportare anche gli svantaggi: istanziamo l’ oggetto che ci serve una volta sola, e lo usiamo tutte le volte che ci serve. La differenza rispetto al primo caso è che non facciamo l’istanza di java.sql.Connection, ma di un oggetto (il pool appunto) che gestisce le nostre richieste di connessione. La connessione già pronta viene ottenuta nel service() (come nel secondo caso), e alla fine di esso viene restituita (non chiusa!). Per fare questo, abbiamo bisogno di scrivere una classe che ‘avvolga’ un oggetto Connection e alcuni altri attributi che ci serviranno per avere ragguagli sullo stato dell’oggetto.
Usando la tecnica che abbiamo descritto il mese scorso, possiamo scrivere una classe simile a questa:

import java.sql.*;
public class PoolableConnection {
  private Connection con = null;
  private boolean locked = false;
  private long calledTime =  0;
 

Il cotruttore, che sarà usato dal pool per fare le istanze di questa classe, valorizza un oggetto Connection:

public PoolableConnection(Connection aConnection){
    if(aConnection != null){
      con = aConnection;
    }
  }

  public Connection getConnection() {
   return con;
  }

Il pool, inoltre, chiama metodo che segue per impostare lo stato di ‘occupato’ di questo oggetto, prima di darlo all’applicazione. Inoltre, l'oggetto memorizza il momento in cui è stato ‘prelevato’. Questo tornerà utile nel caso in cui il pool fallisca nel rilasciare una connessione, caso che affrontiamo più avanti.

public void setLocked(boolean lock){
    locked = lock;
    calledTime = System.currentTimeMillis();
}

quando l’intero pool verrà chiuso, ogni oggetto PoolableConnection provvederà a chiudere la sua connessione con il DB.

public void close() throws SQLException{
    try{
        con.close();
    }
    catch(SQLException sqle){
        System.err.println(sqle.getMessage());
    }
}
 
 

Il sorgente completo (di questa come delle altre classi dell’esempio) è disponibile in questo file zip.

PoolableConnection è scritto appositamente per essere usato da un pool. La classe ConnectionPool, che è il pool vero e proprio, dovrebbe in primo luogo predisporre le connessioni, quindi una lista (un Vector, oppure una LinkedList, oppure una HashTable) in cui inserire in maniera ordinata e indicizzata una collezione di oggetti PoolableConnection. Io ho scelto di usare un vettore, che si presta benissimo ai nostri scopi. Se volessimo subordinare il rilascio delle connessioni a criteri come LIFO o FIFO, allora probabilmente dovremmo anche pensare ad una coda di richieste.
Vediamo le caratteristiche salienti del pool.
In primo luogo definisce una serie di variabili necessarie alla connessione con un database via jdbc.

public class ConnectionPool{

  private String driver;
  private String url;
  private int size;
  private int pctIncrease;
  private String user;
  private String password;
  private Vector pool;
  private java.sql..Driver drv;
  private long maxTimeInUse = 10000;

La prima, intuitiva funzione, di cui questa classe ha bisogno è un metodo per inizializzare il pool:

public synchronized void initializePool() throws Exception{

// test di tutte le variabili necessarie 
// alla connessione: se ne manca anche una sola,
// lanciamo un’eccezione
   if(driver == null){
     throw new Exception("Nome driverjdbc non specificato");
   }

.....

   //creazione delle connessioni
    try{
     drv = (Driver)Class.forName(driver).newInstance();
     DriverManager.registerDriver(drv);

     for(int i = 0;i < size; i++){
       Connection con = createConnection();
       if(con != null){
         PoolableConnection pcon = new PoolableConnection(con);
         addConnection(pcon);
       }
     }
    }
    catch(Exception e){
      throw new Exception("eccezione in initializeConnection: "
                           +e.getMessage());
    }

  }

Quindi, dopo aver fatto l’istanza del pool, la nostra applicazione dovrà valorizzare le variabili del pool e poi chiamare initializePool(). A questo punto il pool è pronto per essere usato. Per ottenere un oggetto connection dal pool, è sufficiente invocare il metodo getConnection(). Vediamo come funziona:
 

public synchronized Connection getConnection() throws Exception  {

    PoolableConnection pcon = null;
    // cerca una connessione disponibile
    for(int i = 0; i < pool.size(); i++){
     pcon = (PoolableConnection)pool.elementAt(i);
     if(!pcon.isLocked()){
       pcon.setLocked(true);
       return pcon.getConnection();
     }
    }

    // Se non trova una connessione libera ne 
    // aggiunge una nuova se ha ancora spazio per farlo
    boolean expand = (size+(size/100*pctIncrease) < pool.size());
    if(expand){
     expandPool();
    }
  }

se le connessioni sono tutte impegnate, getConnection si chiede se gli elementi del vettore sono in numero maggiore rispetto a size più la percentuale massima di incremento. Se non è così, invoca il metodo expand che crea una nuova connessione e la aggiunge al vettore.

La parte più delicata sta nel rilascio della connessione. La volta scorsa parlavamo appunto dei rischi che si celano dietro ad eventi come una non corretta gestione delle eccezioni ed in altre disattenzioni. Qualunque ipotesi di impossibilità di rilasciare la connessione si verifichi, noi dobbiamo sapere come e quando correggerla. Vediamo cosa succede nel metodo releaseConnection():

public synchronized void releaseConnection(Connection con)  {
    boolean foundAndKilled = false;
    for(int i = 0;i < pool.size(); i++){
     PoolableConnection pcon;
     pcon=(PoolableConnection)pool.elementAt(i);
     if(pcon.getConnection() == con){
       System.out.println("sto rilasciando la connessione "+i);
       pcon.setLocked(false);
       foundAndKilled = true;
       break;
     }
    }
    if(!foundAndKilled){
       new ControlThread(maxTimeInUse,pool);
    }
  }

Notate che se dopo aver letto tutto il vettore questo metodo non riesce a rilasciare il lock sull’oggetto che gli è stato passato, invoca un thread che legge di nuovo tutto il vettore, alla ricerca di quelle connessioni per le quali il tempo di impiego si sia prolungato oltre quanto ci si aspetta: questa condizione provoca una forzatura di setLocked(false), in modo che l’oggetto ritorna ad essere disponibile e libero. 
Nella servlet, gestiamo il pool come attributo, istanziandolo ell’init():

Nota: nell’esempio ho utilizzato una url al database di default di Oracle, nell’ipotesi che il motore di servlet (esempio Tomcat) e il server Oracle si trovino  sulla stessa macchina.

ConnectionPool pool = null;

public void init(ServletConfig config) throws ServletException{
    super.init(config);
    try{
        pool = new ConnectionPool();
        ool.setDriver("oracle.jdbc.driver.OracleDriver");
        pool.setURL("jdbc:oracle:thin:@localhost:1521:orcl");
        pool.setUser("scott");
        pool.setPassword("tiger");
        pool.setSize(10);
        pool.setPctIncrease(30);
        pool.initializePool();
    }
    catch(ClassNotFoundException e){
        throw new ServletException(e.toString());
    }
    catch(Exception e){
        throw new ServletException(e.toString());
    }
}

Nel service(),  istanziamo un oggetto Connection:

Connection con = pool.getConnection();

e in una clausola finally, alla fine di tutte le operazioni che implicano la connessione al database, lo rilasciamo:

pool.releaseConnection(con);

L’esempio che segue esegue una select dalla tabella EMP del famoso utente Scott/tiger:

public void service(HttpServletRequest req, 
                    HttpServletResponse res)
                    throws ServletException, IOException{
  res.setContentType("text/html");
  Connection con = null;;
  try{
    //il pool restituisce la prima connessione libera
    con = pool.getConnection();
    PrintWriter out = res.getWriter();
    String html = "<html><body><h1>";
    html+= con.toString();
    html+="<table border=\"1\">";
   
   Statement stat = con.createStatement();
   String sql = "SELECT * FROM emp";
   ResultSet rs = stat.executeQuery(sql);
   
   while (rs.next()){
    html += "<tr><td>"+rs.getString(1)+"</td>";
    html += "<td>"+rs.getString(2)+"</td>";
    html += "<td>"+rs.getString(3)+"</td>";
    html += "<td>"+rs.getString(4)+"</td>";
    html += "<td>"+rs.getString(5)+"</td>";
    html += "<td>"+rs.getString(6)+"</td>";
    html += "<td>"+rs.getString(7)+"</td>";
    html += "<td>"+rs.getString(8)+"</td></tr>";
   }
   html += "</table></body></html>";
   out.print(html);
           
   rs.close();
   stat.close();
   out.close();
   
   }
   catch(Exception e){
    throw new ServletException(e.getMessage(),e);
   }
   finally{
    pool.releaseConnection(con); 
   }
      
   
 }

Se avete accesso ad un database oracle potete provare questo esempio così com’è, altrimenti dovete sostituire sia il nome del driver che la url! Ad esempio, se disponete di MS Access, configurate un driver ODBC da ‘Pannello di Controllo’, ad esempio ‘mio_database’  associato al sample db di access ‘Noorthern Wind’. Quindi il driver diventa:

   “jdbc.odbc.JdbcOdbcDriver”

e la url:
  
       “jdbc:odbc:mio_database”

Se, infine, non disponete di un servlet engine, potete provare il pool come un’applicazione, anche se chiaramente perde molto del suo significato.
Se invece avete un servle engine, provate ad aprire due finestre del browser e a lanciare contemporaneamente la servlet, e vedrete che la pagina conterrà la stampa di due diversi oggetti connection!

Rimane da esaminare il contenuto del metodo destroy() della servlet:

public void destroy(){
  try{
    pool.emptyPool();
  }
  catch(Exception e){
    e.printStackTrace();
  }
}

Quando la servlet viene chiusa dalla VM, anche il pool che essa aveva istanziato viene rilasciato.
Se lanciate questa applicazione su un server Oracle, avete la possibilità di controllare il comportamento della vostra applicazione, monitorando ciò che avviene sul db.
Quando lanciate l’applicazione, un numero size di connessioni vengono aperte. Se conoscete la password dell’utente system o sys, provate la query:

  SELECT username FROM v$session;

e vedrete che size sessioni (connessioni)  sono state aperte a nome dell’utente scott!
Se chiudete l’applicazione, ed eseguite la stessa select, noterete che anche le sessioni di Scott (o dell’utente che avete usato) non esistono più. 

Ci si può domandare ancora una cosa: se il pool viene incrementato in base alla percentuale pctIncrease, è desiderabile che noi manteniamo tutte le connessioni che sono state aperte in più rispetto a size?
Bisognerebbe creare un processo che monitorizzi costantemente il pool e controlli quanto (in termini di tempo e di numero di volte) le connessioni eccedenti la dimensione originale vengono usate! Una procedura del genere sarebbe di per se più complessa del pool (pure migliorabile sotto diversi aspetti) che abbiamo appena descritto.
Tuttavia, si può pensare ad una cosa meno impegnativa, e stabilire ad esempio che nelle connessioni create dopo l’inizializzazione del pool venga valorizzato anche un altro parametro, che memorizzi il tempo per il quale la connessione NON è stata utilizzata! Comunque, la cosa essenziale quando si progetta e si realizza un’applicazione che interagisce con una base dati (in generale quando si creano dei processi asincroni), è impedire che si allochino risorse senza controllo da parte dell’applicazione stessa. 
La gestione degli imprevisti è un altro punto fondamentale: anche se, di regola, il pool che abbiamo scritto non dovrebbe aver bisogno di interventi eccezionali,  non possiamo fare affidamento sulla buona sorte, e dobbiamo gestire i casi, pur remoti, in cui risorse inutilizzate rimangano appese sul server
Infatti, ControlThread scatta subito, ogni volta che il metodo releaseConnection() non riesce a rilasciare un oggetto!

Il mese prossimo affronteremo l’implementazione di un rudimentale server usando un pool di thread che si occuperà di gestire una coda di connessioni.

Vai alla Home Page di MokaByte
Vai alla prima pagina di questo mese


MokaByte®  è un marchio registrato da MokaByte s.r.l.
Java®, Jini®  e tutti i nomi derivati sono marchi registrati da Sun Microsystems; tutti i diritti riservati
E' vietata la riproduzione anche parziale
Per comunicazioni inviare una mail a
mokainfo@mokabyte.it