čtvrtek 28. ledna 2010

Proč nepotřebuji (zatím) asynchronní JDBC

Lukáš Křečan se zkusil zamyslet nad tím, jestli v Jave potřebuje asynchronní JDBC API a svůj závěr vtisknul přímo do článku Proč nepotřebuji asynchronní JDBC. Já si dovolím Lukášem mírně doplnit.

Nejdříve bychom si mohli demonstrovat na jednoduchém kódu Lukášem zmiňované Servlet 3.0 API s podporou asynchronního volání, které bude v našem dalším povídání hrát důležitou roli.

@WebServlet(name = "AsyncServlet", urlPatterns = { "/AsyncServlet" }, asyncSupported=true)
public class AsyncServlet extends HttpServlet {
 
  protected void doGet(HttpServletRequest request, HttpServletResponse response) {
  AsyncContext ac = request.startAsync(request, response);
  ac.start(new Worker(ac));  
 }
 
 public static class Worker implements Runnable {
  private final AsyncContext ac;
  
  public Worker(AsyncContext ac) { 
   this.ac = ac;
  }

  @Override
  public void run() {
   HttpServletResponse response = (HttpServletResponse) ac.getResponse();
   try {
    response.setContentType("text/html;charset=UTF-8");
    PrintWriter out = null;
    try {
     out = response.getWriter();
     out.println("<html>");     
     out.println("<body>");
     out.println("<h1>Servlet AsyncServlet output</h1>");
     for(int i = 0; i < 10000; i++) {
      out.println("<p>Asynchronously generated response :-)</p>");
      out.flush();
      try {
       Thread.sleep(200);       
      } catch (InterruptedException e) {}
     }
     out.println("</body></html>");     
    } catch (IOException e) {     
    } finally {
     out.close();
    }
   } finally {
    ac.complete();
   }
  }
 }
} 
  

Trocha popisu. Na řádku 5 instruujeme kontejner, že požadavek bude vyřízen asynchronně. Na řádku 6 odpálíme asynchronní exekuci. V tomto případě se použije vlákno z poolu servletového kontejneru. Samozřejmě to nemusí vždy vyhovovat a my mužeme vytvořit klasický ThreadPoolExecutor a vlákna brát z něj.

Po vykonání řádku 6 se nám tedy v novém vlákně spustí exekuce viz třídá Worker a původní vlákno servletového kontejneru dokončí metodu doGet a může být použito k obsluze jiného HTTP požadavku. Metoda run třídy Worker tedy běží v jiném vlákně. Pomocí AsyncContextu, který jsme si předali a tvoří nás komunikační kanál se servletovým kontejnerem, jsme schopni vytvořit repsonse. Dokončení asynchronní práce se signalizuje voláním metody complete na řádku 40.

Nyní k jádru pudla. Kde nám do hry vstupuje ono vzývané asynchronní JDBC API. Představme si náš příklad lehce upraven o volání nám dobře známého JDBC. Stačí nám pouze třída Worker.

public static class Worker implements Runnable {
  private final AsyncContext ac;
  
  public Worker(AsyncContext ac) { 
   this.ac = ac;
  }

  @Override
  public void run() {
   Connection conn = getConnectionFromSomewhere();
   PreparedStatement ps =  conn.prepareStatement("insert into orders (a) values ('a')");
   ps.executeUpdate();
      ac.dispatch("/dekujeme-za-nakup.jsp");   
  } 
 }    

Co se stane? Servletový kontejner má vlákno vrácené a může obsluhovat další HTTP požadavky, ale vlákno vykonávající metodu run bude zablokované na řádcích 10 a 12. Jíra Mareš má do jisté míry pravdu, že na řádku 10 nám může pomoci poolovaní na úrovni databázových připojení. Nicméně vlákno bude zablokováno nejpozději na řádku 12.

Představme si situaci, že máme eschop a uživatel právě učinil nákup. Požadavek se odeslal a kód zůstal trčet na řádku 12. Asi nechceme, aby měl uživatel deset sekund "zmrzlé" UI prohlížeče, ale raději bychom mu ukázali stránku kde mu poděkujeme za nákup a třeba ještě něco nabídli z dceřiného obchodu.

Bez asynchronního JDBC API musíme obě blokující volání (10,11) zabalit ručně do asynchronního volání. Původní metoda run potom vypadá přímo pohádkově.

@Override
public void run() {
 new Thread(){    
  public void run() {
   Connection conn = getConnectionFromSomewhere();
   PreparedStatement ps =  conn.prepareStatement("insert into foo (a) values ('a')");
   ps.executeUpdate();
  }    
 }.start();   
 ac.dispatch("/dekujeme-za-nakup.jsp");
} 

Nechtěl bych zde polemizovat, že JDBC je samospasné. Je to jenom špička ledovce. Každopádně je to dobrý začátek k tomu, aby si podobným způsobem nemuseli zaneřádit kód aplikační vývojáři. Samo o sobě asynchronní JDBC nepřinese žádné drastické zvýšení výkonu (v tom jsme s Lukášem za jedno), snad možná svižnější UI. Aby mělo asynchronní JDBC nějaký výkonnostní dopad, bylo by potřeba mít driver a databázi, která asynchronní volání podporuje.

A mimochodem není to taková utopie viz níže