1 /*
  2  * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 /**
 25  * @test
 26  * @summary Test using a custom scheduler as the default virtual thread scheduler
 27  * @requires vm.continuations
 28  * @run junit/othervm -Djdk.virtualThreadScheduler.implClass=CustomDefaultScheduler$CustomScheduler1
 29  *     --enable-native-access=ALL-UNNAMED CustomDefaultScheduler
 30  * @run junit/othervm -Djdk.virtualThreadScheduler.implClass=CustomDefaultScheduler$CustomScheduler2
 31  *     --enable-native-access=ALL-UNNAMED CustomDefaultScheduler
 32  */
 33 
 34 import java.lang.Thread.VirtualThreadScheduler;
 35 import java.util.concurrent.Executor;
 36 import java.util.concurrent.Executors;
 37 import java.util.concurrent.ExecutorService;
 38 import java.util.concurrent.ThreadFactory;
 39 import java.util.concurrent.CountDownLatch;
 40 import java.util.concurrent.atomic.AtomicBoolean;
 41 import java.util.concurrent.atomic.AtomicReference;
 42 import java.util.concurrent.locks.LockSupport;
 43 
 44 import org.junit.jupiter.api.Test;
 45 import org.junit.jupiter.api.BeforeAll;
 46 import static org.junit.jupiter.api.Assertions.*;
 47 import static org.junit.jupiter.api.Assumptions.*;
 48 
 49 class CustomDefaultScheduler {
 50     private static String schedulerClassName;
 51 
 52     @BeforeAll
 53     static void setup() {
 54         schedulerClassName = System.getProperty("jdk.virtualThreadScheduler.implClass");
 55     }
 56 
 57     /**
 58      * Custom scheduler that uses a thread pool.
 59      */
 60     public static class CustomScheduler1 implements VirtualThreadScheduler {
 61         private final ExecutorService pool;
 62 
 63         public CustomScheduler1() {
 64             ThreadFactory factory = Thread.ofPlatform().daemon().factory();
 65             pool = Executors.newFixedThreadPool(1, factory);
 66         }
 67 
 68         @Override
 69         public void execute(Thread thread, Runnable task) {
 70             if (thread.isVirtual()) {
 71                 pool.execute(task);
 72             } else {
 73                 throw new UnsupportedOperationException();
 74             }
 75         }
 76     }
 77 
 78     /**
 79      * Custom scheduler that delegates to the built-in default scheduler.
 80      */
 81     public static class CustomScheduler2 implements VirtualThreadScheduler {
 82         private final VirtualThreadScheduler builtinScheduler;
 83 
 84         public CustomScheduler2(VirtualThreadScheduler builtinScheduler) {
 85             this.builtinScheduler = builtinScheduler;
 86         }
 87 
 88         VirtualThreadScheduler builtinScheduler() {
 89             return builtinScheduler;
 90         }
 91 
 92         @Override
 93         public void execute(Thread vthread, Runnable task) {
 94             builtinScheduler.execute(vthread, task);
 95         }
 96     }
 97 
 98     /**
 99      * Test that a virtual thread uses the custom default scheduler.
100      */
101     @Test
102     void testUseCustomScheduler() throws Exception {
103         var ref = new AtomicReference<VirtualThreadScheduler>();
104         Thread.startVirtualThread(() -> {
105             ref.set(VirtualThreadScheduler.current());
106         }).join();
107         VirtualThreadScheduler scheduler = ref.get();
108         assertEquals(schedulerClassName, scheduler.getClass().getName());
109     }
110 
111     /**
112      * Test virtual thread park/unpark using custom default scheduler.
113      */
114     @Test
115     void testPark() throws Exception {
116         var done = new AtomicBoolean();
117         var thread = Thread.startVirtualThread(() -> {
118             while (!done.get()) {
119                 LockSupport.park();
120             }
121         });
122         try {
123             await(thread, Thread.State.WAITING);
124         } finally {
125             done.set(true);
126             LockSupport.unpark(thread);
127             thread.join();
128         }
129     }
130 
131     /**
132      * Test virtual thread blocking on monitor when using custom default scheduler.
133      */
134     @Test
135     void testBlock() throws Exception {
136         var ready = new CountDownLatch(1);
137         var lock = new Object();
138         var thread = Thread.ofVirtual().unstarted(() -> {
139             ready.countDown();
140             synchronized (lock) {
141             }
142         });
143         synchronized (lock) {
144             thread.start();
145             ready.await();
146             await(thread, Thread.State.BLOCKED);
147         }
148         thread.join();
149     }
150 
151     /**
152      * Test custom default scheduler execute method with bad parameters.
153      */
154     @Test
155     void testExecuteThrows() throws Exception {
156         var ref = new AtomicReference<VirtualThreadScheduler>();
157         Thread vthread = Thread.startVirtualThread(() -> {
158             ref.set(VirtualThreadScheduler.current());
159         });
160         vthread.join();
161         VirtualThreadScheduler scheduler = ref.get();
162 
163         Runnable task = () -> { };
164 
165         // platform thread
166         Thread thread = Thread.ofPlatform().unstarted(() -> { });
167         assertThrows(UnsupportedOperationException.class, () -> scheduler.execute(thread, task));
168 
169         // nulls
170         assertThrows(NullPointerException.class, () -> scheduler.execute(null, task));
171         assertThrows(NullPointerException.class, () -> scheduler.execute(vthread, null));
172     }
173 
174     /**
175      * Test custom default scheduler delegating to builtin default scheduler.
176      */
177     @Test
178     void testDelegatingToBuiltin() throws Exception {
179         assumeTrue(schedulerClassName.equals("CustomDefaultScheduler$CustomScheduler2"));
180 
181         var ref = new AtomicReference<VirtualThreadScheduler>();
182         Thread vthread = Thread.startVirtualThread(() -> {
183             ref.set(VirtualThreadScheduler.current());
184         });
185         vthread.join();
186 
187         var customScheduler1 = new CustomScheduler1();
188         var customScheduler2 = (CustomScheduler2) ref.get();
189         var builtinScheduler = customScheduler2.builtinScheduler();
190 
191         // ensure builtin default scheduler can't be mis-used
192         assertThrows(ClassCastException.class, () -> { var e = (Executor) builtinScheduler; });
193 
194         var vthread0 = Thread.ofVirtual().scheduler(builtinScheduler).unstarted(() -> { });
195         var vthread1 = Thread.ofVirtual().scheduler(customScheduler1).unstarted(() -> { });
196         var vthread2 = Thread.ofVirtual().scheduler(customScheduler2).unstarted(() -> { });
197 
198         Runnable task = () -> { };
199 
200         // builtin scheduler can execute tasks for itself or customScheduler2
201         builtinScheduler.execute(vthread0, task);
202         assertThrows(IllegalArgumentException.class, () -> builtinScheduler.execute(vthread1, task));
203         builtinScheduler.execute(vthread2, task);
204 
205         assertThrows(IllegalArgumentException.class, () -> customScheduler2.execute(vthread1, task));
206         customScheduler2.execute(vthread2, task);
207     }
208 
209     /**
210      * Waits for the given thread to reach a given state.
211      */
212     private void await(Thread thread, Thread.State expectedState) throws InterruptedException {
213         Thread.State state = thread.getState();
214         while (state != expectedState) {
215             assertTrue(state != Thread.State.TERMINATED, "Thread has terminated");
216             Thread.sleep(10);
217             state = thread.getState();
218         }
219     }
220 }