Virtual threads sometimes wake up after multiples of requested sleep duration

XMLWordPrintable

      ADDITIONAL SYSTEM INFORMATION :
      Windows 11

      A DESCRIPTION OF THE PROBLEM :
      I was benchmarking one of open-source libraries for database connection pool. I created a few hundreds of Virtual Threads, each one trying to:
      1. Get the connection from the pool.
      2. Simulate blocking calls by calling Thread.sleep(200).
      3. Return connection back to the pool.

      I noticed that the whole benchmark took longer than I anticipated, so I started investigation. It turns out that sometimes a Virtual Thread calling Thread.sleep doesn't wake up after the time I requested, but after multiples of this time (two times longer, three times longer, etc.).

      I was able to reproduce the issue without this external library, confirming that the library itself is not the source of the problem. I wrote a simple benchmark, polling and returning resources through SynchronousQueue and encountered the same problem with Thread.sleep blocking for multiples of requested time (e.g 400ms, 600ms).

      If I change the implementation to wait for platform threads which internally call Thread.sleep (the code I commented), the issue is solved, so it seems the problem is caused by some bug with waking up Virtual Threads from sleep.



      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Run the code I attached

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      There are no numbers printed - meaning that all Thread.sleep calls unblocked after less than 300ms
      ACTUAL -
      There are some numbers printed - meaning that some Thread.sleep calls unblocked after more than 300ms

      ---------- BEGIN SOURCE ----------
      import java.util.concurrent.ExecutionException;
      import java.util.concurrent.Future;
      import java.util.concurrent.TimeUnit;
      import java.util.concurrent.atomic.AtomicInteger;

      void main() {
          int initialNumberOfResources = 10;
          int numberOfThreads = 300;
          var finishedPolling = new AtomicInteger(0);
          var executorService = Executors.newVirtualThreadPerTaskExecutor();
          // var platformThreadExecutor = Executors.newFixedThreadPool(numberOfThreads);
          var tasks = new ArrayList<Future>();
          var resources = new SynchronousQueue<Object>(true);

          // Try to make 10 resources available
          for (int i = 0; i < initialNumberOfResources; i++) {
              executorService.submit(() -> {
                  try {
                      resources.offer(new Object(), 1, TimeUnit.HOURS);
                  } catch (InterruptedException e) {
                      System.exit(1);
                  }
              });
          }

          // main logic: tries to get a resource from SynchronousQueue,
          // simulates some work on the resource by calling Thread.sleep and then releases the resource to other threads through the SynchronousQueue
          for (int i = 0; i < numberOfThreads; i++) {
              tasks.add(executorService.submit(() -> {
                  try {
                      var resource = resources.poll(1, TimeUnit.HOURS);
                      finishedPolling.incrementAndGet();

                      long sleepStart = System.nanoTime();
                      Thread.sleep(200);
                      // platformThreadExecutor.submit(() -> {
                      // try {
                      // Thread.sleep(200);
                      // } catch (InterruptedException e) {
                      // e.printStackTrace();
                      // System.exit(1);
                      // }
                      // }).get();
                      long sleepEnd = System.nanoTime();
                      long sleepDurationInMs = (sleepEnd - sleepStart) / 1_000_000;
                      if (sleepDurationInMs > 300) { // if sleep duration is unusually long
                          IO.println(sleepDurationInMs);
                      }

                      // try to return resource to the queue as long as there are other working threads
                      while (finishedPolling.get() < numberOfThreads && !resources.offer(resource)) {
                          Thread.sleep(10);
                      }
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                      System.exit(1);
                  }
              }));
          }

          // clean up
          tasks.forEach(task -> {
              try {
                  task.get();
              } catch (InterruptedException | ExecutionException e) {
                  e.printStackTrace();
                  System.exit(1);
              }
          });
          // platformThreadExecutor.shutdownNow();
          executorService.shutdownNow();
          return;
      }
      ---------- END SOURCE ----------

            Assignee:
            Patricia Tavares
            Reporter:
            Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated:
              Resolved: