Announcement Announcement Module
Collapse
No announcement yet.
object pooling Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • object pooling

    Greetings,
    I apologize in advance for the length of this email...

    I am writing a simple prototype in anticipation of converting an EJB app to Spring. This EJB app uses object pooling, so I am trying out Spring's object pooling capabilities. Yes, I realize that singletons are generally preferable, but the app owners want their object pooling...

    I have followed the example outlined in the online Sping docs. And have the following Beans::

    Code:
    <bean id="myModelTarget" 
            class="etrade.services.pool.MyModelImpl" singleton="false" >
        <property name="outString">
          <value>GARBAGE OUT</value>
        </property>
      </bean>
    
      <bean id="myModelSource"
          class="org.springframework.aop.target.CommonsPoolTargetSource">
          <property name="targetBeanName">
             <value>myModelTarget</value>
          </property>
          <property name="maxSize">
             <value>5</value>
          </property>
      </bean>
      <bean id="myModel" 
        class="org.springframework.aop.framework.ProxyFactoryBean">
          <property name="targetSource">
             <ref local="myModelSource" />
          </property>
      </bean>
    Where ModelImpl is a very simple class;

    Code:
    public class MyModelImpl implements MyModel
    &#123;
      static protected org.apache.commons.logging.Log dbglog = 
        org.apache.commons.logging.LogFactory.getLog&#40; "ETDEBUG." + MyModelImpl.class.getName&#40;&#41; &#41;;
    
      static private int objKnt = 0;
      synchronized static private int nextObjKnt&#40;&#41;
      &#123;  
        objKnt++; 
        return objKnt; 
      &#125; 
    
      private String outString = null;
      private int myNum = -1;
    
      public String toString&#40;&#41; 
      &#123; 
        StringBuffer buff = new StringBuffer&#40;&#41;;
        buff.append&#40; "&#123; "  &#41;; 
        buff.append&#40; super.toString&#40;&#41; &#41;; 
        buff.append&#40; " myNum = " + myNum &#41;;
        buff.append&#40; " &#125;"  &#41;;
        return buff.toString&#40;&#41;;
      &#125; 
    
      public MyModelImpl&#40;&#41;
      &#123; 
        super&#40;&#41;; 
        myNum = nextObjKnt&#40;&#41;;
        dbglog.debug&#40;"MyModelImpl CTOR -- created myNum= " + myNum &#41;;
      &#125; 
    
      public String doSomething&#40; String in &#41;
      &#123;
        dbglog.debug&#40;"ENTERED MyModelImpl.doSomething&#40; " + in + " &#41;" &#41;;
        return in + " ==> " + outString + " " + myNum; 
      &#125;
    
      public void setOutString&#40; String os &#41;
      &#123; outString = os; &#125;
    
      public String getOutString&#40;&#41;
      &#123; return outString; &#125;
    &#125;
    But when I run the following JUnit:

    Code:
     public void testPoolLimit&#40;&#41; 
      &#123;
        List modelPool = new ArrayList&#40;&#41;;
    
        ApplicationContext context = MyModelDriver.getAppContext&#40;&#41;;
        
        CommonsPoolTargetSource pool =
          &#40;&#40;CommonsPoolTargetSource&#41;context.getBean&#40;"myModelSource"&#41;&#41;;
        assertTrue&#40;pool.getMaxSize&#40;&#41; == 5&#41;;
        dbglog.debug&#40; "pool.getActiveCount = " + pool.getActiveCount&#40;&#41; &#41;;
        dbglog.debug&#40; "pool.getIdleCount = " + pool.getIdleCount&#40;&#41; &#41;;
        
        for &#40;int i = 0; i <= 10; i++&#41; &#123;
          MyModel model = &#40;MyModel&#41;context.getBean&#40;"myModel"&#41;;
          modelPool.add&#40;model&#41;;
          dbglog.debug&#40; "model = " + model &#41;;
          dbglog.debug&#40; "pool.getActiveCount = " + pool.getActiveCount&#40;&#41; &#41;;
          dbglog.debug&#40; "pool.getIdleCount = " + pool.getIdleCount&#40;&#41; &#41;;
        &#125;
        dbglog.debug&#40; "modelPool.size&#40;&#41; = " + modelPool.size&#40;&#41; &#41;;
       
        //assertTrue&#40;modelPool.size&#40;&#41; < 10&#41;;
      &#125;
    I get this output. Note that I seem to get back the same instance every time. Seems to me that I should be getting back different instances since I haven't returned the values to the pool??

    Code:
      ...................<snip>
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;937&#93;&#40;main     &#41;&#40;DefaultListableBeanFactory&#41;&#91;DEBUG&#93;&#58;&#58; Invoking BeanPostProcessors before initialization of bea
    n 'myModelTarget'
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;937&#93;&#40;main     &#41;&#40;DefaultListableBeanFactory&#41;&#91;DEBUG&#93;&#58;&#58; Invoking BeanPostProcessors after initialization of bean
     'myModelTarget'
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;937&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; model = &#123; etrade.services.pool.MyModelImpl@1d62270 myNum = 2 &#125;
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;937&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getActiveCount = 0
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;937&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getIdleCount = 1
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;957&#93;&#40;main     &#41;&#40;DefaultListableBeanFactory&#41;&#91;DEBUG&#93;&#58;&#58; Returning cached instance of singleton bean 'myModel'
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;957&#93;&#40;main     &#41;&#40;DefaultListableBeanFactory&#41;&#91;DEBUG&#93;&#58;&#58; Bean with name 'myModel' is a factory bean
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;957&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; model = &#123; etrade.services.pool.MyModelImpl@1d62270 myNum = 2 &#125;
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;957&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getActiveCount = 0
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;957&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getIdleCount = 1
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;986&#93;&#40;main     &#41;&#40;DefaultListableBeanFactory&#41;&#91;DEBUG&#93;&#58;&#58; Returning cached instance of singleton bean 'myModel'
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;986&#93;&#40;main     &#41;&#40;DefaultListableBeanFactory&#41;&#91;DEBUG&#93;&#58;&#58; Bean with name 'myModel' is a factory bean
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;986&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; model = &#123; etrade.services.pool.MyModelImpl@1d62270 myNum = 2 &#125;
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;986&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getActiveCount = 0
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;986&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getIdleCount = 1
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;09&#58;6&#93;&#40;main     &#41;&#40;DefaultListableBeanFactory&#41;&#91;DEBUG&#93;&#58;&#58; Returning cached instance of singleton bean 'myModel'
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;09&#58;6&#93;&#40;main     &#41;&#40;DefaultListableBeanFactory&#41;&#91;DEBUG&#93;&#58;&#58; Bean with name 'myModel' is a factory bean
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;09&#58;6&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; model = &#123; etrade.services.pool.MyModelImpl@1d62270 myNum = 2 &#125;
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;09&#58;6&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getActiveCount = 0
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;09&#58;6&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getIdleCount = 1
    ................ <snip>
    I did notice this in the output. Do I have this wired wrong??

    Code:
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;422&#93;&#40;main     &#41;&#40;DefaultListableBeanFactory&#41;&#91;DEBUG&#93;&#58;&#58; Invoking setBeanFactory on BeanFactoryAware bean 'myMode
    l'
        &#91;junit&#93; &#91;06/01/05 09&#58;23&#58;08&#58;422&#93;&#40;main     &#41;&#40;ProxyFactoryBean&#41;&#91;DEBUG&#93;&#58;&#58;Not refreshing target&#58; bean name not specified in interceptorNames
    It is entirely possible that I am missing something basic. I'm a relative Spring Newbie...

    Also, I'm a bit confused. How does Spring release objects back to the pool??

    Thanks,
    -- Chris

  • #2
    Greetings,
    I have a bit more input. I've implemented an explicit commons-pool GenericObjectPool and can demonstrate the behavior I would expect. In this case the off-the-shelf GenericObjectPool blocks when the pool is full...
    I must really be missing something??
    Does anyone know how this works in Spring??
    Thanks,
    -- Chris

    The JUnit

    Code:
      public void testNonSpringPool&#40;&#41; 
      &#123;
        GenericObjectPool pool = new GenericObjectPool&#40; new MyModelFactory&#40;&#41; &#41;;
        pool.setMaxActive&#40; 5 &#41;;
        pool.setMaxWait&#40; 5000 &#41;;
    
        dbglog.debug&#40; "pool.getActiveCount = " + pool.getNumActive&#40;&#41; &#41;;
        dbglog.debug&#40; "pool.getIdleCount = " + pool.getNumIdle&#40;&#41; &#41;;
    
        // Note&#58; we are not returning the Object to the pool so that it should fail...
        try &#123; 
          List modelPool = new ArrayList&#40;&#41;;
          
          for &#40;int i = 0; i <= 10; i++&#41; &#123;
            MyModel model = &#40;MyModel&#41;pool.borrowObject&#40;&#41;;
            modelPool.add&#40;model&#41;;
            dbglog.debug&#40; "model = " + model &#41;;
            dbglog.debug&#40; "pool.getActiveCount = " + pool.getNumActive&#40;&#41; &#41;;
            dbglog.debug&#40; "pool.getIdleCount = " + pool.getNumIdle&#40;&#41; &#41;;
          &#125;
          dbglog.debug&#40; "modelPool.size&#40;&#41; = " + modelPool.size&#40;&#41; &#41;;
        &#125;
        catch &#40; Exception ee &#41; &#123;
          dbglog.error&#40; ee &#41;;
          fail&#40;&#41;;
        &#125;
    And the required Factory

    Code:
    public class MyModelFactory extends BasePoolableObjectFactory 
    &#123; 
      // for makeObject we'll simply return a new buffer 
      public Object makeObject&#40;&#41; 
      &#123; return new MyModelImpl&#40;&#41;; &#125; 
      
      // when an object is returned to the pool,  
      // we'll clear it out 
      public void passivateObject&#40;Object obj&#41; &#123;&#125; 
      
      // for all other methods, the no-op  
      // implementation in BasePoolableObjectFactory 
      // will suffice 
    &#125;
    And the output. Note that an Exception is thrown when the JUnit times out after hitting the maximum...

    Code:
        &#91;junit&#93; Running etrade.services.pool.TestMyModel
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;223&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getActiveCount = 0
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;223&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getIdleCount = 0
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;223&#93;&#40;main     &#41;&#40;MyModelImpl     &#41;&#91;DEBUG&#93;&#58;&#58; MyModelImpl CTOR -- created myNum= 1
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;223&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; model = &#123; etrade.services.pool.MyModelImpl@1592174 myNum = 1 &#125;
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;223&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getActiveCount = 1
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;233&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getIdleCount = 0
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;233&#93;&#40;main     &#41;&#40;MyModelImpl     &#41;&#91;DEBUG&#93;&#58;&#58; MyModelImpl CTOR -- created myNum= 2
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;233&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; model = &#123; etrade.services.pool.MyModelImpl@a352a5 myNum = 2 &#125;
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;233&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getActiveCount = 2
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;233&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getIdleCount = 0
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;233&#93;&#40;main     &#41;&#40;MyModelImpl     &#41;&#91;DEBUG&#93;&#58;&#58; MyModelImpl CTOR -- created myNum= 3
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;233&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; model = &#123; etrade.services.pool.MyModelImpl@86fe26 myNum = 3 &#125;
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;233&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getActiveCount = 3
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;233&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getIdleCount = 0
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;253&#93;&#40;main     &#41;&#40;MyModelImpl     &#41;&#91;DEBUG&#93;&#58;&#58; MyModelImpl CTOR -- created myNum= 4
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;253&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; model = &#123; etrade.services.pool.MyModelImpl@97a560 myNum = 4 &#125;
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;253&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getActiveCount = 4
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;263&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getIdleCount = 0
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;263&#93;&#40;main     &#41;&#40;MyModelImpl     &#41;&#91;DEBUG&#93;&#58;&#58; MyModelImpl CTOR -- created myNum= 5
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;263&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; model = &#123; etrade.services.pool.MyModelImpl@1f3aa07 myNum = 5 &#125;
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;273&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getActiveCount = 5
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;04&#58;273&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;DEBUG&#93;&#58;&#58; pool.getIdleCount = 0
        &#91;junit&#93; &#91;06/01/05 16&#58;01&#58;09&#58;280&#93;&#40;main     &#41;&#40;TestMyModel     &#41;&#91;ERROR&#93;&#58;&#58; java.util.NoSuchElementException&#58; Timeout waiting for idle object
        &#91;junit&#93; Tests run&#58; 1, Failures&#58; 1, Errors&#58; 0, Time elapsed&#58; 5.097 sec
        &#91;junit&#93; Testsuite&#58; etrade.services.pool.TestMyModel
        &#91;junit&#93; Tests run&#58; 1, Failures&#58; 1, Errors&#58; 0, Time elapsed&#58; 5.097 sec
        &#91;junit&#93;
        &#91;junit&#93; Testcase&#58; testNonSpringPool&#40;etrade.services.pool.TestMyModel&#41;&#58;      FAILED
        &#91;junit&#93; null
        &#91;junit&#93; junit.framework.AssertionFailedError
        &#91;junit&#93;     at etrade.services.pool.TestMyModel.testNonSpringPool&#40;TestMyModel.java&#58;113&#41;
        &#91;junit&#93;     at sun.reflect.NativeMethodAccessorImpl.invoke0&#40;Native Method&#41;
        &#91;junit&#93;     at sun.reflect.NativeMethodAccessorImpl.invoke&#40;NativeMethodAccessorImpl.java&#58;39&#41;
        &#91;junit&#93;     at sun.reflect.DelegatingMethodAccessorImpl.invoke&#40;DelegatingMethodAccessorImpl.java&#58;25&#41;
        &#91;junit&#93;

    Comment


    • #3
      I read in another related topic that these instances are created per Thread. So I wrote a simple JUnit to prove it (see below). This does appear to be somewhat true. With one strange caveat.

      Spring object pooling synopsis;

      1) On a given Thread the same instance is returned every time (i.e. by getBean()). (I'm still unclear on when/how objects are returned to teh pool)

      2) A new instance is returned (by getBean()) per Thread up to maxSize -- after which it returns the same instance to every subsequent Thread. So if maxSize=5, the first 5 Threads get a unique instance (say, o1 thru o5), and all subsequent Threads get the same instance (say, o6)

      This seems like odd behavior -- sort of a per Thread singleton.

      In commons-pool, one can specify one of three behaviors when the pool is exhausted, with the default being that it blocks...

      I wish that someone in the know would confirm this behavior. (I see it to be emperically true).
      Thanks,
      -- Chris


      Code:
      public class SpringThread extends Thread 
        &#123;
          public void run&#40;&#41; 
          &#123; 
            ApplicationContext context = MyModelDriver.getAppContext&#40;&#41;;
            MyModel model = &#40;MyModel&#41;context.getBean&#40;"myModel"&#41;;
            dbglog.debug&#40; "GET model = " + model &#41;;
            try &#123; Thread.sleep&#40;5000&#41;; &#125;
            catch &#40; InterruptedException ee &#41; &#123;&#125;
            dbglog.debug&#40; "TERMINATE model = " + model &#41;;
          &#125;
        &#125;
      
        public void testSpringOnThread&#40;&#41;
        &#123;
          try &#123; 
            SpringThread st = null;
            
            for &#40; int i = 1; i <= 10; i++ &#41; &#123;
              st = new SpringThread&#40;&#41;;
              st.start&#40;&#41;;
            &#125;
            Thread.sleep&#40;2000&#41;;
          &#125;
          catch &#40; Exception ee &#41; &#123;
            ee.printStackTrace&#40;&#41;;
            dbglog.error&#40; ee &#41;;
            fail&#40;&#41;;
          &#125;
        &#125;

      Comment


      • #4
        Chris,

        It's not clear from your post if you want to implement pooling for any reason beyond performance improvements, so thought it might be useful to throw in a few other ideas.

        Did you weigh up moving to Hibernate and just using the 1st + 2nd level cache?

        Also, what appserver are you using. JBoss and several other appservers provide (clustered) pooling out of the box for entity beans.

        I mean, even if you get pooling going, it's not going to be clusterable (if that's even a requirement?) whereas Hibernate and the appserver options would be.

        Greg

        Comment


        • #5
          Greg,
          Thanks for your reply. Basically there is an existing app which uses EJBs (and EJB object pooling) which I'd like to port to simple POJOs in Tomcat. The app owners want to keep their object-pooling and I am trying to make that possible.

          IMO, the object-pooling w/ Spring is quite odd. Perhaps I'm using it wrong. That's entirely possible since I'm a relative newbie -- but my experiments illustrate bizarre behavior (IMO).

          Meanwhile I came across ThreadLocalTargetSource which seems to meet my needs. Since Tomcat pools worker Threads and ThreadLocalTargetSource yields an instance per Thread (for the life of that Thread), I think I get what I'm after. (And my experiments thus far justify this opinion). Essentially I offload the pooling to Tomcat...

          Although I would still love it if some Spring expert would let me know what' up with the Spring object-pooling. I still think I've done something wrong -- or just don't understand this -- because IMO the behavior is simply too odd to be correct...

          Thanks,
          -- Chris

          Comment


          • #6
            Hi Chris,

            The app owners want to keep their object-pooling
            It'd be interesting to hear *why* they want to retain object pooling.

            I'm pretty sure Spring object pooling wasn't intended to pool domain objects.

            If the Spring Team commented on this thread, I'd bet they'd also recommend not building your own domain caching mechanism, but use J2EE application server EJB pooling or Hibernate 1st+2nd level caching instead -- which works better than anything most people would ever hope to write.

            In case you haven't seen that part of the Hibernate doco yet, The 1st level cache will cache data within a single request (TheadLocal) and the 2nd level cache will cache data cross multiple requests irrespective of the user making the request.

            Sounds like you've found a way to implement a 1st level cache. Will you be needing a 2nd level cache?

            Comment


            • #7
              spring object pooling

              You can visit this web page: http://blog.arendsen.net/index.php/2...art-i-pooling/

              It provides the answer to spring pooling exactly the way you want..

              Comment

              Working...
              X