/** tests timer behavior in a WebPage. */
  @Test
  public void addToWebPage() {
    Duration dur = Duration.seconds(20);
    final MyAjaxSelfUpdatingTimerBehavior timer = new MyAjaxSelfUpdatingTimerBehavior(dur);
    final MockPageWithLinkAndComponent page = new MockPageWithLinkAndComponent();
    Label label = new Label(MockPageWithLinkAndComponent.COMPONENT_ID, "Hello");
    page.add(label);
    page.add(
        new Link<Void>(MockPageWithLinkAndComponent.LINK_ID) {
          private static final long serialVersionUID = 1L;

          @Override
          public void onClick() {
            // do nothing, link is just used to simulate a roundtrip
          }
        });
    label.setOutputMarkupId(true);
    label.add(timer);

    tester.startPage(page);

    validate(timer, true);

    tester.clickLink(MockPageWithLinkAndComponent.LINK_ID);

    validate(timer, true);
  }
  /** Tests timer behavior in a component added to an AjaxRequestTarget */
  @Test
  public void addToAjaxUpdate() {
    Duration dur = Duration.seconds(20);
    final MyAjaxSelfUpdatingTimerBehavior timer = new MyAjaxSelfUpdatingTimerBehavior(dur);
    final MockPageWithLinkAndComponent page = new MockPageWithLinkAndComponent();

    page.add(new WebComponent(MockPageWithLinkAndComponent.COMPONENT_ID).setOutputMarkupId(true));

    page.add(
        new AjaxLink<Void>(MockPageWithLinkAndComponent.LINK_ID) {
          private static final long serialVersionUID = 1L;

          @Override
          public void onClick(AjaxRequestTarget target) {
            WebMarkupContainer wmc =
                new WebMarkupContainer(MockPageWithLinkAndComponent.COMPONENT_ID);
            wmc.setOutputMarkupId(true);
            wmc.add(timer);
            page.replace(wmc);
            target.add(wmc);
          }
        });

    tester.startPage(page);
    tester.clickLink(MockPageWithLinkAndComponent.LINK_ID);

    validate(timer, false);
  }
  /** @throws Exception */
  @Test
  public void testSerialization() throws Exception {
    // a simple worker that acquires a lock on page 5
    class Locker extends Thread {
      private final PageAccessSynchronizer sync;

      public Locker(PageAccessSynchronizer sync) {
        this.sync = sync;
      }

      @Override
      public void run() {
        sync.lockPage(5);
      }
    }

    // set up a synchronizer and lock page 5 with locker1
    final Duration timeout = Duration.seconds(30);
    final PageAccessSynchronizer sync = new PageAccessSynchronizer(timeout);
    Locker locker1 = new Locker(sync);

    final long start = System.currentTimeMillis();
    locker1.run();

    // make sure we can serialize the synchronizer

    final PageAccessSynchronizer sync2 = WicketObjects.cloneObject(sync);
    assertTrue(sync != sync2);

    // make sure the clone does not retain locks by attempting to lock page locked by locker1 in
    // locker2
    Locker locker2 = new Locker(sync2);
    locker2.run();
    assertTrue(Duration.milliseconds(System.currentTimeMillis() - start).lessThan(timeout));
  }
 /**
  * Verifies that {@link StoredResponsesMap} will expire the oldest entry if it is older than 2
  * seconds
  *
  * @throws Exception
  */
 @Test
 public void entriesLife2Seconds() throws Exception {
   StoredResponsesMap map = new StoredResponsesMap(1000, Duration.seconds(2));
   assertEquals(0, map.size());
   map.put("1", new BufferedWebResponse(null));
   assertEquals(1, map.size());
   TimeUnit.SECONDS.sleep(3);
   map.put("2", new BufferedWebResponse(null));
   assertEquals(1, map.size());
   assertTrue(map.containsKey("2"));
 }
  /** @throws Exception */
  @Test
  public void testBlocking() throws Exception {
    final PageAccessSynchronizer sync = new PageAccessSynchronizer(Duration.seconds(5));
    final Duration hold = Duration.seconds(1);
    final Time t1locks[] = new Time[1];
    final Time t2locks[] = new Time[1];

    class T1 extends Thread {
      @Override
      public void run() {
        sync.lockPage(1);
        t1locks[0] = Time.now();
        hold.sleep();
        sync.unlockAllPages();
      }
    }

    class T2 extends Thread {
      @Override
      public void run() {
        sync.lockPage(1);
        t2locks[0] = Time.now();
        sync.unlockAllPages();
      }
    }

    T1 t1 = new T1();
    t1.setName("t1");
    T2 t2 = new T2();
    t2.setName("t2");
    t1.start();
    Duration.milliseconds(100).sleep();
    t2.start();

    t1.join();
    t2.join();

    assertTrue(!t2locks[0].before(t1locks[0].add(hold)));
  }
    @Override
    protected void onInitialize() {
      ContentViewOrEditPanel contentViewOrEditPanel =
          new ContentViewOrEditPanel("contentViewOrEditPanel", (IModel<Product>) getDefaultModel());
      SubCategoryViewOrEditPanel subCategoryViewOrEditPanel =
          new SubCategoryViewOrEditPanel(
              "subCategoryViewOrEditPanel", (IModel<Product>) getDefaultModel());
      Form<Product> productEditForm = new Form<Product>("productEditForm");
      productEditForm.setModel(
          new CompoundPropertyModel<Product>((IModel<Product>) getDefaultModel()));

      productEditForm.add(new TextField<String>("number"));
      productEditForm.add(new TextField<String>("name"));
      productEditForm.add(new TextArea<String>("description"));
      productEditForm.add(
          new UrlTextField(ITEM_URL, new PropertyModel<String>(getDefaultModelObject(), ITEM_URL)));
      productEditForm.add(new NumberTextField<Integer>("amount"));
      productEditForm.add(new NumberTextField<Integer>("discount"));
      productEditForm.add(new NumberTextField<Integer>("shippingCost"));
      productEditForm.add(new NumberTextField<Integer>("tax"));
      productEditForm.add(new NumberTextField<Integer>("itemHeight"));
      productEditForm.add(new TextField<String>("itemHeightUnit"));
      productEditForm.add(new NumberTextField<Integer>("itemLength"));
      productEditForm.add(new TextField<String>("itemLengthUnit"));
      productEditForm.add(new NumberTextField<Integer>("itemWeight"));
      productEditForm.add(new TextField<String>("itemWeightUnit"));
      productEditForm.add(new NumberTextField<Integer>("itemWidth"));
      productEditForm.add(new TextField<String>("itemWidthUnit"));
      productEditForm.add(new BootstrapCheckbox("bestsellers"));
      productEditForm.add(new BootstrapCheckbox("latestCollection"));
      productEditForm.add(new NumberTextField<Integer>("rating"));
      productEditForm.add(new BootstrapCheckbox("recommended"));
      productEditForm.add(new NumberTextField<Integer>("stock.maxQuantity"));
      productEditForm.add(new NumberTextField<Integer>("stock.minQuantity"));
      productEditForm.add(new NumberTextField<Integer>("stock.quantity"));

      add(productEditForm.setOutputMarkupId(true));
      add(
          contentViewOrEditPanel
              .add(contentViewOrEditPanel.new ContentEditFragement())
              .setOutputMarkupId(true));
      add(
          subCategoryViewOrEditPanel
              .add(subCategoryViewOrEditPanel.new SubCategoryEditFragement())
              .setOutputMarkupId(true));
      add(new NotificationPanel("feedback").hideAfter(Duration.seconds(5)).setOutputMarkupId(true));
      add(new CancelAjaxLink().setOutputMarkupId(true));
      add(new SaveAjaxButton(productEditForm).setOutputMarkupId(true));
      super.onInitialize();
    }
  /** https://issues.apache.org/jira/browse/WICKET-4009 */
  @Test
  public void unlockIfNoSuchPage() {
    PageAccessSynchronizer synchronizer = new PageAccessSynchronizer(Duration.seconds(2));
    IPageManager pageManager = new MockPageManager();
    IPageManager synchronizedPageManager = synchronizer.adapt(pageManager);
    synchronizedPageManager.getPage(0);
    ConcurrentMap<Integer, PageLock> locks = synchronizer.getLocks().get();
    PageLock pageLock = locks.get(Integer.valueOf(0));
    assertNull(pageLock);

    int pageId = 1;
    IManageablePage page = new MockPage(pageId);
    synchronizedPageManager.touchPage(page);
    synchronizedPageManager.getPage(pageId);
    PageLock pageLock2 = locks.get(Integer.valueOf(pageId));
    assertNotNull(pageLock2);
  }
  /**
   * https://issues.apache.org/jira/browse/WICKET-5316
   *
   * @throws Exception
   */
  @Test
  public void failToReleaseUnderLoad() throws Exception {
    final Duration duration = Duration.seconds(20); /* seconds */
    final ConcurrentLinkedQueue<Exception> errors = new ConcurrentLinkedQueue<Exception>();
    final long endTime = System.currentTimeMillis() + duration.getMilliseconds();

    // set the synchronizer timeout one second longer than the test runs to prevent
    // starvation to become an issue
    final PageAccessSynchronizer sync =
        new PageAccessSynchronizer(duration.add(Duration.ONE_SECOND));

    final CountDownLatch latch = new CountDownLatch(100);
    for (int count = 0; count < 100; count++) {
      new Thread() {
        @Override
        public void run() {
          try {
            while (System.currentTimeMillis() < endTime) {
              try {
                logger.debug(Thread.currentThread().getName() + " locking");
                sync.lockPage(0);
                Thread.sleep(1);
                logger.debug(Thread.currentThread().getName() + " locked");
                sync.unlockAllPages();
                logger.debug(Thread.currentThread().getName() + " unlocked");
                Thread.sleep(5);
              } catch (InterruptedException e) {
                throw new RuntimeException(e);
              }
            }
          } catch (Exception e) {
            logger.error(e.getMessage(), e);
            errors.add(e);
          } finally {
            latch.countDown();
          }
        }
      }.start();
    }
    latch.await();
    if (!errors.isEmpty()) {
      logger.error("Number of lock errors that occurred: {}", errors.size());
      throw errors.remove();
    }
  }
  /**
   * <a href="https://issues.apache.org/jira/browse/WICKET-3769">WICKET-3769</a>
   *
   * @throws Exception
   */
  @Test
  public void applicationAndSessionAreExported() throws Exception {
    // bind the session so it can be found in TestSessionFilter
    tester.getSession().bind();

    // execute TestSessionFilter in different thread so that the Application and the Session are
    // not set by WicketTester
    Thread testThread =
        new Thread(
            new Runnable() {
              public void run() {
                try {
                  TestSessionFilter sessionFilter = new TestSessionFilter(tester);

                  Assert.assertFalse(Application.exists());
                  Assert.assertFalse(Session.exists());

                  sessionFilter.doFilter(
                      tester.getRequest(), tester.getResponse(), new TestFilterChain());

                  Assert.assertFalse(Application.exists());
                  Assert.assertFalse(Session.exists());

                } catch (Exception e) {
                  throw new RuntimeException(e.getMessage(), e);
                }
              }
            });

    final StringBuilder failMessage = new StringBuilder();
    final AtomicBoolean passed = new AtomicBoolean(true);

    testThread.setUncaughtExceptionHandler(
        new UncaughtExceptionHandler() {
          public void uncaughtException(Thread t, Throwable e) {
            failMessage.append(e.getMessage());
            passed.set(false);
          }
        });
    testThread.start();
    testThread.join(Duration.seconds(1).getMilliseconds());

    Assert.assertTrue(failMessage.toString(), passed.get());
  }
  /**
   * <a href="https://issues.apache.org/jira/browse/WICKET-3736">WICKET-3736</a>
   *
   * <p>Tries to simulate heavy load on the {@link StoredResponsesMap} by putting many entries and
   * removing randomly them.
   *
   * <p>The test is disabled by default because it is slow (~ 30secs). Enable it when we have
   * categorized tests ({@link Category}) and run slow ones only at Apache CI servers
   *
   * @throws InterruptedException
   */
  @Test
  public void heavyLoad() throws InterruptedException {
    final int numberOfThreads = 100;
    final int iterations = 1000;
    final CountDownLatch startLatch = new CountDownLatch(numberOfThreads);
    final CountDownLatch endLatch = new CountDownLatch(numberOfThreads);
    final SecureRandom rnd = new SecureRandom();
    final StoredResponsesMap map = new StoredResponsesMap(1000, Duration.seconds(60));
    final List<String> keys = new CopyOnWriteArrayList<String>();

    final Runnable r =
        new Runnable() {
          @Override
          public void run() {
            startLatch.countDown();
            try {
              // wait all threads before starting the test
              startLatch.await();
            } catch (InterruptedException e) {
              throw new RuntimeException(e);
            }

            for (int i = 0; i < iterations; i++) {
              String key = "abc" + (rnd.nextDouble() * iterations);
              keys.add(key);
              map.put(key, new BufferedWebResponse(null));

              int randomMax = keys.size() - 1;
              int toRemove = randomMax == 0 ? 0 : rnd.nextInt(randomMax);
              String key2 = keys.get(toRemove);
              map.remove(key2);
            }
            endLatch.countDown();
          }
        };

    for (int t = 0; t < numberOfThreads; t++) {
      new Thread(r).start();
    }
    endLatch.await();
  }
 public UpdateStateBehavior(String event) {
   super(event);
   setThrottleDelay(Duration.seconds(0.4));
 }
示例#12
0
 public AsyncUpdatePanel(String id, IModel<V> callableParameterModel) {
   this(id, callableParameterModel, Duration.seconds(DEFAULT_TIMER_DURATION));
 }
  /**
   * @param pages
   * @param workers
   * @param duration
   * @throws Exception
   */
  public void runContentionTest(final int pages, final int workers, final Duration duration)
      throws Exception {
    final PageAccessSynchronizer sync = new PageAccessSynchronizer(Duration.seconds(1));

    final AtomicInteger[] counts = new AtomicInteger[pages];
    for (int i = 0; i < counts.length; i++) {
      counts[i] = new AtomicInteger();
    }

    final AtomicInteger hits = new AtomicInteger();

    final String[] error = new String[1];

    class Worker extends Thread {
      @Override
      public void run() {
        Random random = new Random();
        Time start = Time.now();

        while (start.elapsedSince().lessThan(duration) && error[0] == null) {
          logger.info(
              "{} elapsed: {}, duration: {}",
              new Object[] {Thread.currentThread().getName(), start.elapsedSince(), duration});
          int page1 = random.nextInt(counts.length);
          int page2 = random.nextInt(counts.length);
          int count = 0;
          while (page2 == page1 && count < 100) {
            page2 = random.nextInt(counts.length);
            count++;
          }
          if (page2 == page1) {
            throw new RuntimeException("orly?");
          }
          try {
            sync.lockPage(page1);
            sync.lockPage(page2);
            // have locks, increment the count

            counts[page1].incrementAndGet();
            counts[page2].incrementAndGet();
            hits.incrementAndGet();

            // hold the lock for some time
            try {
              Thread.sleep(50);
            } catch (InterruptedException e) {
              error[0] = "Worker :" + Thread.currentThread().getName() + " interrupted";
            }

            // decrement the counts
            counts[page1].decrementAndGet();
            counts[page2].decrementAndGet();

            // release lock
          } catch (CouldNotLockPageException e) {
            // ignore
          } finally {
            sync.unlockAllPages();
          }
        }
      }
    }

    class Monitor extends Thread {
      volatile boolean stop = false;

      @Override
      public void run() {
        while (!stop && error[0] == null) {
          for (int i = 0; i < counts.length; i++) {
            int count = counts[i].get();

            if (count < 0 || count > 1) {
              error[0] = "Detected count of: " + count + " for page: " + i;
              return;
            }
          }
          try {
            Thread.sleep(1);
          } catch (InterruptedException e) {
            error[0] = "Monitor thread interrupted";
          }
        }
      }
    }

    Monitor monitor = new Monitor();
    monitor.setName("monitor");
    monitor.start();

    Worker[] bots = new Worker[workers];
    for (int i = 0; i < bots.length; i++) {
      bots[i] = new Worker();
      bots[i].setName("worker " + i);
      bots[i].start();
    }

    for (Worker bot : bots) {
      bot.join();
    }

    monitor.stop = true;
    monitor.join();

    assertNull(error[0], error[0]);
    assertTrue(hits.get() >= counts.length);
  }
 /** @throws Exception */
 @Test
 public void testReentrant() throws Exception {
   final PageAccessSynchronizer sync = new PageAccessSynchronizer(Duration.seconds(5));
   sync.lockPage(0);
   sync.lockPage(0);
 }
@Category(SlowTests.class)
public class PageAccessSynchronizerTest extends Assert {
  private static final Logger logger = LoggerFactory.getLogger(PageAccessSynchronizerTest.class);

  /** */
  @Rule public Timeout globalTimeout = new Timeout((int) Duration.seconds(30).getMilliseconds());

  /** @throws Exception */
  @Test
  public void testReentrant() throws Exception {
    final PageAccessSynchronizer sync = new PageAccessSynchronizer(Duration.seconds(5));
    sync.lockPage(0);
    sync.lockPage(0);
  }

  /** @throws Exception */
  @Test
  public void testBlocking() throws Exception {
    final PageAccessSynchronizer sync = new PageAccessSynchronizer(Duration.seconds(5));
    final Duration hold = Duration.seconds(1);
    final Time t1locks[] = new Time[1];
    final Time t2locks[] = new Time[1];

    class T1 extends Thread {
      @Override
      public void run() {
        sync.lockPage(1);
        t1locks[0] = Time.now();
        hold.sleep();
        sync.unlockAllPages();
      }
    }

    class T2 extends Thread {
      @Override
      public void run() {
        sync.lockPage(1);
        t2locks[0] = Time.now();
        sync.unlockAllPages();
      }
    }

    T1 t1 = new T1();
    t1.setName("t1");
    T2 t2 = new T2();
    t2.setName("t2");
    t1.start();
    Duration.milliseconds(100).sleep();
    t2.start();

    t1.join();
    t2.join();

    assertTrue(!t2locks[0].before(t1locks[0].add(hold)));
  }

  /**
   * @param pages
   * @param workers
   * @param duration
   * @throws Exception
   */
  public void runContentionTest(final int pages, final int workers, final Duration duration)
      throws Exception {
    final PageAccessSynchronizer sync = new PageAccessSynchronizer(Duration.seconds(1));

    final AtomicInteger[] counts = new AtomicInteger[pages];
    for (int i = 0; i < counts.length; i++) {
      counts[i] = new AtomicInteger();
    }

    final AtomicInteger hits = new AtomicInteger();

    final String[] error = new String[1];

    class Worker extends Thread {
      @Override
      public void run() {
        Random random = new Random();
        Time start = Time.now();

        while (start.elapsedSince().lessThan(duration) && error[0] == null) {
          logger.info(
              "{} elapsed: {}, duration: {}",
              new Object[] {Thread.currentThread().getName(), start.elapsedSince(), duration});
          int page1 = random.nextInt(counts.length);
          int page2 = random.nextInt(counts.length);
          int count = 0;
          while (page2 == page1 && count < 100) {
            page2 = random.nextInt(counts.length);
            count++;
          }
          if (page2 == page1) {
            throw new RuntimeException("orly?");
          }
          try {
            sync.lockPage(page1);
            sync.lockPage(page2);
            // have locks, increment the count

            counts[page1].incrementAndGet();
            counts[page2].incrementAndGet();
            hits.incrementAndGet();

            // hold the lock for some time
            try {
              Thread.sleep(50);
            } catch (InterruptedException e) {
              error[0] = "Worker :" + Thread.currentThread().getName() + " interrupted";
            }

            // decrement the counts
            counts[page1].decrementAndGet();
            counts[page2].decrementAndGet();

            // release lock
          } catch (CouldNotLockPageException e) {
            // ignore
          } finally {
            sync.unlockAllPages();
          }
        }
      }
    }

    class Monitor extends Thread {
      volatile boolean stop = false;

      @Override
      public void run() {
        while (!stop && error[0] == null) {
          for (int i = 0; i < counts.length; i++) {
            int count = counts[i].get();

            if (count < 0 || count > 1) {
              error[0] = "Detected count of: " + count + " for page: " + i;
              return;
            }
          }
          try {
            Thread.sleep(1);
          } catch (InterruptedException e) {
            error[0] = "Monitor thread interrupted";
          }
        }
      }
    }

    Monitor monitor = new Monitor();
    monitor.setName("monitor");
    monitor.start();

    Worker[] bots = new Worker[workers];
    for (int i = 0; i < bots.length; i++) {
      bots[i] = new Worker();
      bots[i].setName("worker " + i);
      bots[i].start();
    }

    for (Worker bot : bots) {
      bot.join();
    }

    monitor.stop = true;
    monitor.join();

    assertNull(error[0], error[0]);
    assertTrue(hits.get() >= counts.length);
  }

  /** @throws Exception */
  @Test
  public void testConcurrency() throws Exception {
    runContentionTest(20, 10, Duration.seconds(10));
  }

  /** @throws Exception */
  @Test
  public void testContention() throws Exception {
    runContentionTest(10, 20, Duration.seconds(10));
  }

  /** @throws Exception */
  @Test
  public void testSerialization() throws Exception {
    // a simple worker that acquires a lock on page 5
    class Locker extends Thread {
      private final PageAccessSynchronizer sync;

      public Locker(PageAccessSynchronizer sync) {
        this.sync = sync;
      }

      @Override
      public void run() {
        sync.lockPage(5);
      }
    }

    // set up a synchronizer and lock page 5 with locker1
    final Duration timeout = Duration.seconds(30);
    final PageAccessSynchronizer sync = new PageAccessSynchronizer(timeout);
    Locker locker1 = new Locker(sync);

    final long start = System.currentTimeMillis();
    locker1.run();

    // make sure we can serialize the synchronizer

    final PageAccessSynchronizer sync2 = WicketObjects.cloneObject(sync);
    assertTrue(sync != sync2);

    // make sure the clone does not retain locks by attempting to lock page locked by locker1 in
    // locker2
    Locker locker2 = new Locker(sync2);
    locker2.run();
    assertTrue(Duration.milliseconds(System.currentTimeMillis() - start).lessThan(timeout));
  }

  /** https://issues.apache.org/jira/browse/WICKET-4009 */
  @Test
  public void unlockIfNoSuchPage() {
    PageAccessSynchronizer synchronizer = new PageAccessSynchronizer(Duration.seconds(2));
    IPageManager pageManager = new MockPageManager();
    IPageManager synchronizedPageManager = synchronizer.adapt(pageManager);
    synchronizedPageManager.getPage(0);
    ConcurrentMap<Integer, PageLock> locks = synchronizer.getLocks().get();
    PageLock pageLock = locks.get(Integer.valueOf(0));
    assertNull(pageLock);

    int pageId = 1;
    IManageablePage page = new MockPage(pageId);
    synchronizedPageManager.touchPage(page);
    synchronizedPageManager.getPage(pageId);
    PageLock pageLock2 = locks.get(Integer.valueOf(pageId));
    assertNotNull(pageLock2);
  }

  /**
   * https://issues.apache.org/jira/browse/WICKET-5316
   *
   * @throws Exception
   */
  @Test
  public void failToReleaseUnderLoad() throws Exception {
    final Duration duration = Duration.seconds(20); /* seconds */
    final ConcurrentLinkedQueue<Exception> errors = new ConcurrentLinkedQueue<Exception>();
    final long endTime = System.currentTimeMillis() + duration.getMilliseconds();

    // set the synchronizer timeout one second longer than the test runs to prevent
    // starvation to become an issue
    final PageAccessSynchronizer sync =
        new PageAccessSynchronizer(duration.add(Duration.ONE_SECOND));

    final CountDownLatch latch = new CountDownLatch(100);
    for (int count = 0; count < 100; count++) {
      new Thread() {
        @Override
        public void run() {
          try {
            while (System.currentTimeMillis() < endTime) {
              try {
                logger.debug(Thread.currentThread().getName() + " locking");
                sync.lockPage(0);
                Thread.sleep(1);
                logger.debug(Thread.currentThread().getName() + " locked");
                sync.unlockAllPages();
                logger.debug(Thread.currentThread().getName() + " unlocked");
                Thread.sleep(5);
              } catch (InterruptedException e) {
                throw new RuntimeException(e);
              }
            }
          } catch (Exception e) {
            logger.error(e.getMessage(), e);
            errors.add(e);
          } finally {
            latch.countDown();
          }
        }
      }.start();
    }
    latch.await();
    if (!errors.isEmpty()) {
      logger.error("Number of lock errors that occurred: {}", errors.size());
      throw errors.remove();
    }
  }
}
  /**
   * Validates the response, then makes sure the timer injects itself again when called. Tests
   * {@link AbstractAjaxTimerBehavior#restart(AjaxRequestTarget)} method
   *
   * <p>WICKET-1525, WICKET-2152
   */
  public void testRestartMethod() {
    final Integer labelInitialValue = Integer.valueOf(0);

    final Label label =
        new Label(MockPageWithLinkAndComponent.COMPONENT_ID, new Model<Integer>(labelInitialValue));

    // the duration doesn't matter because we manually trigger the behavior
    final AbstractAjaxTimerBehavior timerBehavior =
        new AbstractAjaxTimerBehavior(Duration.seconds(2)) {
          private static final long serialVersionUID = 1L;

          @Override
          protected void onTimer(AjaxRequestTarget target) {
            // increment the label's model object
            label.setDefaultModelObject(((Integer) label.getDefaultModelObject()) + 1);
            target.add(label);
          }
        };

    final MockPageWithLinkAndComponent page = new MockPageWithLinkAndComponent();
    page.add(label);
    page.add(
        new AjaxLink<Void>(MockPageWithLinkAndComponent.LINK_ID) {
          private static final long serialVersionUID = 1L;

          @Override
          public void onClick(AjaxRequestTarget target) {
            if (timerBehavior.isStopped()) {
              timerBehavior.restart(target);
            } else {
              timerBehavior.stop(target);
            }
          }
        });

    label.setOutputMarkupId(true);
    label.add(timerBehavior);

    tester.startPage(page);

    final String labelPath = MockPageWithLinkAndComponent.COMPONENT_ID;

    // assert label == initial value (i.e. 0)
    tester.assertLabel(labelPath, String.valueOf(labelInitialValue));

    // increment to 1
    tester.executeBehavior(timerBehavior);

    // assert label == 1
    tester.assertLabel(labelPath, String.valueOf(labelInitialValue + 1));

    // stop the timer
    tester.clickLink(MockPageWithLinkAndComponent.LINK_ID);

    // trigger it, but it is stopped
    tester.executeBehavior(timerBehavior);

    // assert label is still 1
    tester.assertLabel(labelPath, String.valueOf(labelInitialValue + 1));

    // restart the timer
    tester.clickLink(MockPageWithLinkAndComponent.LINK_ID);

    // increment to 2
    tester.executeBehavior(timerBehavior);

    // assert label is now 2
    tester.assertLabel(labelPath, String.valueOf(labelInitialValue + 2));
  }
 /** @throws Exception */
 @Test
 public void testConcurrency() throws Exception {
   runContentionTest(20, 10, Duration.seconds(10));
 }
 /** @throws Exception */
 @Test
 public void testContention() throws Exception {
   runContentionTest(10, 20, Duration.seconds(10));
 }
 public void start() {
   if (timer == null) {
     timer = new Timer("Random Data Updater", true);
     timer.scheduleAtFixedRate(this, 0, Duration.seconds(5).getMilliseconds());
   }
 }
/**
 * Behavior used to enqueue triggers and send them to the client using timer based polling.
 *
 * <p>The polling interval is configured in the constructor. The more frequent is the polling, the
 * more quickly your client will be updated, but also the more you will load your server and your
 * network.
 *
 * <p>A timeout can also be configured to indicate when the behavior should consider the page has
 * been disconnected. This is important to clean appropriately the resources associated with the
 * page.
 *
 * @author Xavier Hanin
 */
public class TimerChannelBehavior extends AbstractAjaxTimerBehavior implements Serializable {

  private static final long serialVersionUID = 1L;

  private static final AtomicLong COUNTER = new AtomicLong();

  private static Method[] methods;

  private static final int ADD_COMPONENT_METHOD = 0;

  private static final int ADD_COMPONENT_WITH_MARKUP_ID_METHOD = 1;

  private static final int APPEND_JAVASCRIPT_METHOD = 2;

  private static final int PREPEND_JAVASCRIPT_METHOD = 3;

  private static final int FOCUS_COMPONENT_METHOD = 4;

  /** The default margin after a polling interval to consider the page is disconnected */
  static final Duration TIMEOUT_MARGIN = Duration.seconds(5);

  static {
    try {
      methods =
          new Method[] {
            AjaxRequestTarget.class.getMethod("add", new Class[] {Component[].class}),
            AjaxRequestTarget.class.getMethod("add", new Class[] {Component.class, String.class}),
            AjaxRequestTarget.class.getMethod("appendJavaScript", new Class[] {CharSequence.class}),
            AjaxRequestTarget.class.getMethod(
                "prependJavaScript", new Class[] {CharSequence.class}),
            AjaxRequestTarget.class.getMethod("focusComponent", new Class[] {Component.class}),
          };
    } catch (final Exception e) {
      throw new WicketRuntimeException("Unable to initialize DefaultAjaxPushBehavior", e);
    }
  }

  /**
   * This class is used to store a list of delayed method calls.
   *
   * <p>The method calls are actually calls to methods on {@link AjaxRequestTarget}, which are
   * invoked when the client polls the server.
   *
   * @author Xavier Hanin
   */
  private static class DelayedMethodCallList implements Serializable {
    private static final long serialVersionUID = 1L;

    private final Application _application;

    /**
     * Used to store a method and its parameters to be later invoked on an object.
     *
     * @author Xavier Hanin
     */
    private class DelayedMethodCall implements Serializable {
      private static final long serialVersionUID = 1L;

      /** The index of the method to invoke We store only an index to avoid serialization issues */
      private final int m;
      /** the parameters to use when the method is called */
      private final Object[] parameters;

      /**
       * Construct.
       *
       * @param m the index of the method to be called
       * @param parameters the parameters to use when the method is called
       */
      public DelayedMethodCall(final int m, final Object[] parameters) {
        this.m = m;
        this.parameters = parameters;
      }

      /**
       * Invokes the method with the parameters on the given object.
       *
       * @see java.lang.reflect.Method#invoke(Object, Object[])
       * @param o the object on which the method should be called
       * @throws IllegalArgumentException
       * @throws IllegalAccessException
       * @throws InvocationTargetException
       */
      public void invoke(final Object o)
          throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
        final Application originalApplication = Application.get();
        try {
          ThreadContext.setApplication(_application);
          methods[m].invoke(o, parameters);
        } finally {
          ThreadContext.setApplication(originalApplication);
        }
      }
    }

    /** stores the list of {@link DelayedMethodCall} to invoke */
    private final List<DelayedMethodCall> calls;

    /** Construct. */
    public DelayedMethodCallList() {
      _application = Application.get();
      calls = new ArrayList<DelayedMethodCall>();
    }

    /**
     * Construct a copy of the given {@link DelayedMethodCallList}.
     *
     * @param dmcl
     */
    public DelayedMethodCallList(final DelayedMethodCallList dmcl) {
      _application = Application.get();
      calls = new ArrayList<DelayedMethodCall>(dmcl.calls);
    }

    /**
     * Add a {@link DelayedMethodCall} to the list
     *
     * @param m the index of the method to be later invoked
     * @param parameters the parameters to use when the method will be invoked
     */
    public void addCall(final int m, final Object[] parameters) {
      calls.add(new DelayedMethodCall(m, parameters));
    }

    /**
     * Invokes all the {@link DelayedMethodCall} in the list on the given Object
     *
     * @see java.lang.reflect.Method#invoke(Object, Object[])
     * @param o the object on which delayed methods should be called
     * @throws IllegalArgumentException
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     */
    public void invoke(final Object o)
        throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
      for (final DelayedMethodCall dmc : calls) {
        dmc.invoke(o);
      }
    }

    /**
     * Indicates if this list is empty or not
     *
     * @return true if this list is empty, false otherwise
     */
    public boolean isEmpty() {
      return calls.isEmpty();
    }

    /** Used to remove all the delayed methods from this list */
    public void clear() {
      calls.clear();
    }
  }

  /**
   * An {@link IPushTarget} implementation which enqueue {@link DelayedMethodCallList}, also called
   * triggers, for a {@link TimerChannelBehavior} identified by its id.
   *
   * <p>TimerPushTarget are thread safe, and can be used from any thread. Since it is not
   * serializable, it is not intended to be stored in a wicket component.
   *
   * @author Xavier Hanin
   */
  public static class TimerPushTarget implements IPushTarget {
    /**
     * A trigger currently being constructed, waiting for a call to trigger to go to the triggers
     * list.
     */
    private final DelayedMethodCallList currentTrigger = new DelayedMethodCallList();
    /** The Wicket Application in which this target is used */
    private final Application application;
    /** The id of the behavior to which this target corresponds */
    private final String id;
    /**
     * The duration to wait before considering that a page is not connected any more This is usually
     * set to the polling interval + a safety margin
     */
    private final Duration timeout;

    public TimerPushTarget(final Application application, final String id, final Duration timeout) {
      super();
      this.application = application;
      this.id = id;
      this.timeout = timeout;
    }

    /**
     * Adds the component.
     *
     * @param component the component
     */
    public void addComponent(final Component component) {
      synchronized (currentTrigger) {
        currentTrigger.addCall(ADD_COMPONENT_METHOD, new Object[] {component});
      }
    }

    /**
     * Adds the component.
     *
     * @param component the component
     * @param markupId the markup id
     */
    public void addComponent(final Component component, final String markupId) {
      synchronized (currentTrigger) {
        currentTrigger.addCall(
            ADD_COMPONENT_WITH_MARKUP_ID_METHOD, new Object[] {component, markupId});
      }
    }

    /**
     * Append java script.
     *
     * @param javascript the javascript
     */
    public void appendJavaScript(final String javascript) {
      synchronized (currentTrigger) {
        currentTrigger.addCall(APPEND_JAVASCRIPT_METHOD, new Object[] {javascript});
      }
    }

    /**
     * Focus component.
     *
     * @param component the component
     */
    public void focusComponent(final Component component) {
      synchronized (currentTrigger) {
        currentTrigger.addCall(FOCUS_COMPONENT_METHOD, new Object[] {component});
      }
    }

    /**
     * Prepend java script.
     *
     * @param javascript the javascript
     */
    public void prependJavaScript(final String javascript) {
      synchronized (currentTrigger) {
        currentTrigger.addCall(PREPEND_JAVASCRIPT_METHOD, new Object[] {javascript});
      }
    }

    /** Trigger. */
    public void trigger() {
      DelayedMethodCallList trigger = null;
      synchronized (currentTrigger) {
        if (currentTrigger.isEmpty()) {
          return;
        }
        trigger = new DelayedMethodCallList(currentTrigger);
        currentTrigger.clear();
      }
      final List<DelayedMethodCallList> triggers = getTriggers();
      synchronized (triggers) {
        triggers.add(trigger);
      }
    }

    public boolean isConnected() {
      return TimerChannelBehavior.isConnected(application, id, timeout);
    }

    /**
     * Methods used to access the triggers queued for the the behavior to which this target
     * corresponds.
     *
     * @return a List of triggers queued for the current component
     */
    private List<DelayedMethodCallList> getTriggers() {
      return TimerChannelBehavior.getTriggers(application, id);
    }
  }

  private final String id;
  private final Duration timeout;

  /**
   * Construct a TimerChannelBehavior which actually refreshes the clients by polling the server for
   * changes at the given duration.
   *
   * @param updateInterval the interval at which the server should be polled for changes
   */
  public TimerChannelBehavior(final Duration updateInterval) {
    this(updateInterval, updateInterval.add(TIMEOUT_MARGIN));
  }

  /**
   * Construct a TimerChannelBehavior which actually refreshes the clients by polling the server for
   * changes at the given duration.
   *
   * @param updateInterval the interval at which the server should be polled for changes
   * @param timeout The timeout to set
   */
  public TimerChannelBehavior(final Duration updateInterval, final Duration timeout) {
    super(updateInterval);
    id = String.valueOf(COUNTER.incrementAndGet());
    this.timeout = timeout;
  }

  @Override
  protected void onBind() {
    super.onBind();
    touch(getComponent().getApplication(), id);
  }

  /** @see AbstractAjaxTimerBehavior#onTimer(AjaxRequestTarget) */
  @Override
  protected void onTimer(final AjaxRequestTarget target) {
    touch(getComponent().getApplication(), id);
    final List<DelayedMethodCallList> triggers = getTriggers(getComponent().getApplication(), id);
    List<DelayedMethodCallList> triggersCopy;
    synchronized (triggers) {
      if (triggers.isEmpty()) {
        return;
      }
      triggersCopy = new ArrayList<DelayedMethodCallList>(triggers);
      triggers.clear();
    }
    for (final DelayedMethodCallList dmcl : triggersCopy) {
      try {
        dmcl.invoke(target);
      } catch (final Exception e) {
        throw new WicketRuntimeException(
            "a problem occured while adding events to AjaxRequestTarget", e);
      }
    }
  }

  /**
   * Creates a new push target to which triggers can be sent
   *
   * @return an IPushTarget to which triggers can be sent in any thread.
   */
  public IPushTarget newPushTarget() {
    return new TimerPushTarget(Application.get(), id, timeout);
  }

  public void renderHead(Component component, IHeaderResponse response) {
    touch(getComponent().getApplication(), id);
    final String timerChannelPageId =
        getComponent().getPage().getId() + ":updateInterval:" + getUpdateInterval();
    if (!getPageId(getComponent().getApplication(), id).equals(id)) {
      // behavior has already been redirected, we can skip this rendering
      return;
    }
    if (!response.wasRendered(timerChannelPageId)) {
      super.renderHead(component, response);
      setRedirectId(getComponent().getApplication(), timerChannelPageId, id);
      response.markRendered(timerChannelPageId);
    } else {
      /*
       * A similar behavior has already been rendered, we have no need to
       * render ourself All we need is redirect our own behavior id to the
       * id of the behavior which has been rendered.
       */
      final String redirectedId = getPageId(getComponent().getApplication(), timerChannelPageId);
      setRedirectId(getComponent().getApplication(), id, redirectedId);
    }
  }

  /** Meta data key for queued triggers, stored by page behavior id */
  static final MetaDataKey<ConcurrentMap<String, List<DelayedMethodCallList>>> TRIGGERS_KEY =
      new MetaDataKey<ConcurrentMap<String, List<DelayedMethodCallList>>>() {
        private static final long serialVersionUID = 1L;
      };

  /** Meta data key for poll events time, stored by page behavior id */
  static final MetaDataKey<ConcurrentMap<String, Time>> EVENTS_KEY =
      new MetaDataKey<ConcurrentMap<String, Time>>() {
        private static final long serialVersionUID = 1L;
      };

  /** Meta data key for page behavior ids, stored by behavior id */
  static final MetaDataKey<ConcurrentMap<String, String>> PAGE_ID_KEY =
      new MetaDataKey<ConcurrentMap<String, String>>() {
        private static final long serialVersionUID = 1L;
      };

  public static boolean isConnected(
      final Application application, final String id, final Duration timeout) {
    final Time time = TimerChannelBehavior.getLastPollEvent(application, id);
    boolean isConnected;
    if (time == null) {
      // the behavior has been cleaned
      return false;
    }
    isConnected = time.elapsedSince().compareTo(timeout) < 0;
    if (!isConnected) {
      // timeout expired, the page is probably not connected anymore

      // we clean the metadata to avoid memory leak
      TimerChannelBehavior.cleanMetadata(application, id);
    }
    return isConnected;
  }

  /**
   * Methods used to access the triggers queued for the behavior
   *
   * <p>The implementation uses a Map stored in the application, where the behavior id is the key,
   * because these triggers cannot be stored in component instance or the behavior itself, since
   * they may be serialized and deserialized.
   *
   * @param application the application in which the triggers are stored
   * @param id the id of the behavior
   * @return a List of triggers queued for the component
   */
  private static List<DelayedMethodCallList> getTriggers(final Application application, String id) {
    id = getPageId(application, id);
    ConcurrentMap<String, List<DelayedMethodCallList>> triggersById;
    synchronized (application) {
      triggersById = application.getMetaData(TRIGGERS_KEY);
      if (triggersById == null) {
        triggersById = new ConcurrentHashMap<String, List<DelayedMethodCallList>>();
        application.setMetaData(TRIGGERS_KEY, triggersById);
      }
    }
    List<DelayedMethodCallList> triggers = triggersById.get(id);
    if (triggers == null) {
      triggersById.putIfAbsent(id, new ArrayList<DelayedMethodCallList>());
      triggers = triggersById.get(id);
    }
    return triggers;
  }

  /**
   * Cleans the metadata (triggers, poll time) associated with a given behavior id
   *
   * @param application the application in which the metadata are stored
   * @param id the id of the behavior
   */
  private static void cleanMetadata(final Application application, String id) {
    id = getPageId(application, id);
    ConcurrentMap<String, List<DelayedMethodCallList>> triggersById = null;
    ConcurrentMap<String, Time> eventsTimeById = null;
    ConcurrentMap<String, String> pageIdsById = null;
    synchronized (application) {
      triggersById = application.getMetaData(TRIGGERS_KEY);
      eventsTimeById = application.getMetaData(EVENTS_KEY);
      pageIdsById = application.getMetaData(PAGE_ID_KEY);
    }
    if (triggersById != null) {
      final List<DelayedMethodCallList> triggers = triggersById.remove(id);
      if (triggers != null) {
        synchronized (triggers) {
          triggers.clear();
        }
      }
    }
    if (eventsTimeById != null) {
      eventsTimeById.remove(id);
    }
    if (pageIdsById != null) {
      pageIdsById.remove(id);
    }
  }

  private static void touch(final Application application, String id) {
    id = getPageId(application, id);
    ConcurrentMap<String, Time> eventsTimeById;
    synchronized (application) {
      eventsTimeById = application.getMetaData(EVENTS_KEY);
      if (eventsTimeById == null) {
        eventsTimeById = new ConcurrentHashMap<String, Time>();
        application.setMetaData(EVENTS_KEY, eventsTimeById);
      }
    }
    eventsTimeById.put(id, Time.now());
  }

  private static Time getLastPollEvent(final Application application, String id) {
    id = getPageId(application, id);
    ConcurrentMap<String, Time> eventsTimeById;
    synchronized (application) {
      eventsTimeById = application.getMetaData(EVENTS_KEY);
      if (eventsTimeById == null) {
        return null;
      }
    }
    final Time time = eventsTimeById.get(id);
    return time;
  }

  /**
   * Returns the page behavior id corresponding the given behavior id. Only one behavior is actually
   * rendered on a page for the same updateInterval, to optimize the number of requests. Therefore
   * all timer channel behaviors of the same page are redirected to the same id, using this method.
   *
   * @param application the wicket application to which the behavior belong
   * @param id the id of the behavior for which the page behavior id should be found
   * @return the page behavior id corresponding the given behavior id.
   */
  private static String getPageId(final Application application, final String id) {
    ConcurrentMap<String, String> pageIdsById;
    synchronized (application) {
      pageIdsById = application.getMetaData(PAGE_ID_KEY);
      if (pageIdsById == null) {
        return id;
      }
    }
    final String pageId = pageIdsById.get(id);
    return pageId == null ? id : pageId;
  }

  private static void setRedirectId(
      final Application application, final String id, final String redirectedId) {
    ConcurrentMap<String, String> pageIdsById;
    synchronized (application) {
      pageIdsById = application.getMetaData(PAGE_ID_KEY);
      if (pageIdsById == null) {
        pageIdsById = new ConcurrentHashMap<String, String>();
        application.setMetaData(PAGE_ID_KEY, pageIdsById);
      }
    }
    final String oldRedirectedId = pageIdsById.put(id, redirectedId);
    if (!redirectedId.equals(oldRedirectedId)) {
      /*
       * The id was not already redirected to the redirectedId, we need to
       * merge the information before redirection with information after
       * redirection
       */
      final String idToRedirect = oldRedirectedId == null ? id : oldRedirectedId;
      redirect(application, idToRedirect, redirectedId);
    }
  }

  private static void redirect(
      final Application application, final String idToRedirect, final String redirectedId) {
    ConcurrentMap<String, List<DelayedMethodCallList>> triggersById = null;
    ConcurrentMap<String, Time> eventsTimeById = null;
    synchronized (application) {
      triggersById = application.getMetaData(TRIGGERS_KEY);
      eventsTimeById = application.getMetaData(EVENTS_KEY);
    }
    if (triggersById != null) {
      final List<DelayedMethodCallList> triggersToRedirect = triggersById.remove(idToRedirect);
      if (triggersToRedirect != null) {
        // we redirect triggers to the new list, in two steps, to avoid
        // acquiring
        // locks on two triggers simultaneously, which would be a source
        // of risk of
        // dead locks
        List<DelayedMethodCallList> triggersToRedirectCopy;
        synchronized (triggersToRedirect) {
          triggersToRedirectCopy = new ArrayList<DelayedMethodCallList>(triggersToRedirect);
          triggersToRedirect.clear();
        }
        if (!triggersToRedirectCopy.isEmpty()) {
          final List<DelayedMethodCallList> triggers = getTriggers(application, redirectedId);
          synchronized (triggers) {
            triggers.addAll(triggersToRedirectCopy);
          }
        }
      }
    }
    if (eventsTimeById != null) {
      eventsTimeById.remove(idToRedirect);
      /*
       * we don't need to merge touch information, since merged behaviors
       * always have the same touch rates
       */
    }
  }

  @Override
  public String toString() {
    return "TimerChannelBehavior::" + id;
  }

  public String getId() {
    return id;
  }
}