1 /*
   2  * Copyright (c) 2018, 2019, 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  * @run testng Scopes
  27  * @summary Basic tests for java.lang.FiberScope
  28  */
  29 import java.time.Duration;
  30 import java.time.Instant;
  31 import java.util.concurrent.*;
  32 import java.util.concurrent.locks.LockSupport;
  33 import java.lang.FiberScope.Option;
  34 import static java.lang.FiberScope.Option.*;
  35 
  36 import org.testng.annotations.Test;
  37 import static org.testng.Assert.*;
  38 
  39 @Test
  40 public class Scopes {
  41 
  42     // -- background scope --
  43 
  44     @Test(expectedExceptions = { IllegalCallerException.class })
  45     public void testBackground1() {
  46         FiberScope.background().close();
  47     }
  48 
  49     // Fibers scheduled in the background scope can be cancelled
  50     public void testBackground2() throws Exception {
  51         var fiber = FiberScope.background().schedule(() -> {
  52             assertFalse(Fiber.cancelled());
  53             Fiber.current().map(Fiber::cancel);
  54             assertTrue(Fiber.cancelled());
  55         });
  56         fiber.join();
  57     }
  58 
  59     // Cancellation is not propagated to fibers in the background scope
  60     public void testBackground3() throws Exception {
  61         try (var scope = FiberScope.open(PROPAGATE_CANCEL)) {
  62             scope.schedule(() -> {
  63                 var fiber = FiberScope.background().schedule(() -> LockSupport.park());
  64                 Fiber.current().map(Fiber::cancel);
  65                 assertFalse(fiber.isCancelled());
  66             }).join();
  67         }
  68     }
  69 
  70     // Cancellation is not propagated to fibers in the background scope
  71     public void testBackground4() throws Exception {
  72         runInFiber(() -> testBackground3());
  73     }
  74 
  75     // test that close waits
  76     public void testBasic1() throws Exception {
  77         Fiber<?> fiber;
  78         try (var scope = FiberScope.open()) {
  79             fiber = scope.schedule(() -> {
  80                 TimeUnit.SECONDS.sleep(1);
  81                 return null;
  82             });
  83         }
  84         assertFalse(fiber.isAlive());
  85         assertTrue(fiber.join() == null);
  86     }
  87 
  88     // test that close waits when owner thread is interrupted
  89     public void testBasic2() throws Exception {
  90         Fiber<?> fiber;
  91         try (var scope = FiberScope.open()) {
  92             fiber = scope.schedule(() -> {
  93                 TimeUnit.SECONDS.sleep(1);
  94                 return null;
  95             });
  96             Thread.currentThread().interrupt();
  97         } finally {
  98             assertTrue(Thread.interrupted());
  99         }
 100         assertFalse(fiber.isAlive());
 101         assertTrue(fiber.join() == null);
 102     }
 103 
 104     // test that close waits when owner fiber is interrupted
 105     public void testBasic3() throws Exception {
 106         runInFiber(() -> testBasic2());
 107     }
 108 
 109     // test that close waits when owner fiber is cancelled
 110     public void testBasic4() throws Exception {
 111         runInFiber(() -> {
 112             Fiber.current().map(Fiber::cancel);
 113             Fiber<?> fiber;
 114             try (var scope = FiberScope.open()) {
 115                 fiber = scope.schedule(() -> {
 116                     TimeUnit.SECONDS.sleep(1);
 117                     return null;
 118                 });
 119             }
 120             assertFalse(fiber.isAlive());
 121             Thread.interrupted(); // clear interrupt status
 122             assertTrue(fiber.join() == null);
 123         });
 124     }
 125 
 126     // test scheduling a fiber in an enclosing scope
 127     public void testBasic5() {
 128         try (var scope1 = FiberScope.open()) {
 129             try (var scope2 = FiberScope.open()) {
 130                 scope1.schedule(() -> "foo");
 131             }
 132         }
 133     }
 134 
 135     // test scheduling a fiber when not in the scope
 136     @Test(expectedExceptions = { IllegalCallerException.class })
 137     public void testBasic6() {
 138         try (var scope1 = FiberScope.open()) {
 139             var scope2 = FiberScope.open();
 140             scope2.close();
 141             scope2.schedule(() -> "foo");
 142         }
 143     }
 144 
 145 
 146     // test that CANCEL_AT_CLOSE cancels all fibers at close
 147     public void testCancelAtClose1() {
 148         Fiber<?> fiber;
 149         try (var scope = FiberScope.open(CANCEL_AT_CLOSE)) {
 150             fiber = scope.schedule(() -> LockSupport.park());
 151         }
 152         assertFalse(fiber.isAlive());
 153         assertTrue(fiber.isCancelled());
 154     }
 155 
 156     public void testCancelAtClose2() throws Exception {
 157         runInFiber(() -> testCancelAtClose1());
 158     }
 159 
 160     // test Fiber.cancelled in a IGNORE_CANCEL scope
 161     public void testIgnoreCancel1() throws Exception {
 162         runInFiber(() -> {
 163             Fiber.current().map(Fiber::cancel);
 164             assertTrue(Fiber.cancelled());
 165             try (var scope = FiberScope.open(IGNORE_CANCEL)) {
 166                 assertFalse(Fiber.cancelled());
 167             }
 168             assertTrue(Fiber.cancelled());
 169         });
 170     }
 171 
 172     // test Fiber.cancelled when enclosing scope is IGNORE_CANCEL
 173     public void testIgnoreCancel2() throws Exception {
 174         runInFiber(() -> {
 175             Fiber.current().map(Fiber::cancel);
 176             assertTrue(Fiber.cancelled());
 177             try (var scope1 = FiberScope.open(IGNORE_CANCEL)) {
 178                 assertFalse(Fiber.cancelled());
 179                 // IGNORE_CANCEL should be inherited
 180                 try (var scope2 = FiberScope.open()) {
 181                     assertFalse(Fiber.cancelled());
 182                 }
 183                 assertFalse(Fiber.cancelled());
 184             }
 185             assertTrue(Fiber.cancelled());
 186         });
 187     }
 188 
 189     // test Fiber.cancelled when enclosing scope is IGNORE_CANCEL
 190     public void testIgnoreCancel3() throws Exception {
 191         runInFiber(() -> {
 192             Fiber.current().map(Fiber::cancel);
 193             assertTrue(Fiber.cancelled());
 194             try (var scope1 = FiberScope.open(IGNORE_CANCEL)) {
 195                 assertFalse(Fiber.cancelled());
 196                 Instant deadline = Instant.now().plusSeconds(2);
 197                 try (var scope2 = FiberScope.open(deadline)) {
 198                     assertTrue(Fiber.cancelled());
 199                 }
 200                 assertFalse(Fiber.cancelled());
 201             }
 202             assertTrue(Fiber.cancelled());
 203         });
 204     }
 205 
 206     // test that cancellation is propagated to fibers scheduled in a scope
 207     public void testPropagateCancel1() throws Exception {
 208         runInFiber(() -> {
 209             try (var scope = FiberScope.open(PROPAGATE_CANCEL)) {
 210                 var fiber = scope.schedule(() -> {
 211                     LockSupport.park();
 212                     assertTrue(Fiber.cancelled());
 213                 });
 214 
 215                 // give fiber time to park
 216                 Thread.sleep(500);
 217                 Fiber.current().map(Fiber::cancel);
 218 
 219                 Thread.interrupted(); // clear interrupt status
 220                 fiber.join();
 221             }
 222         });
 223     }
 224 
 225     // test scheduling a new fiber when the scope owner has been cancelled
 226     public void testPropagateCancel2() throws Exception {
 227         runInFiber(() -> {
 228             try (var scope = FiberScope.open(PROPAGATE_CANCEL)) {
 229                 Fiber.current().map(Fiber::cancel);
 230 
 231                 // fiber should be scheduled with the cancel (and interrupt) status set
 232                 var fiber = scope.schedule(() -> LockSupport.park());
 233                 assertTrue(fiber.isCancelled());
 234 
 235                 Thread.interrupted(); // clear interrupt status
 236                 fiber.join();
 237             }
 238         });
 239     }
 240 
 241     // check cancellation with a nested scope
 242     public void testPropagateCancel3() throws Exception {
 243         runInFiber(() -> {
 244             try (var scope1 = FiberScope.open(PROPAGATE_CANCEL)) {
 245                 Fiber<?> top = Fiber.current().orElseThrow();
 246                 var child = scope1.schedule(() -> {
 247                     try (var scope2 = FiberScope.open()) {
 248                         assertFalse(Fiber.cancelled());
 249                         top.cancel();
 250                         assertTrue(Fiber.cancelled());
 251                     }
 252                 });
 253                 joinUninterruptibly(child);
 254             }
 255         });
 256     }
 257 
 258     // check cancellation with a deeply nested scope
 259     public void testPropagateCancel4() throws Exception {
 260         runInFiber(() -> {
 261             try (var scope1 = FiberScope.open(PROPAGATE_CANCEL)) {
 262                 Fiber<?> top = Fiber.current().orElseThrow();
 263                 var child = scope1.schedule(() -> {
 264                     try (var scope2 = FiberScope.open()) {
 265                         var grandchild = scope2.schedule(() -> {
 266                             assertFalse(Fiber.cancelled());
 267                             top.cancel();
 268                             assertTrue(Fiber.cancelled());
 269                         });
 270                         joinUninterruptibly(grandchild);
 271                         return null;
 272                     }
 273                 });
 274                 joinUninterruptibly(child);
 275             }
 276         });
 277     }
 278 
 279     // -- deadlines --
 280 
 281     // thread owner
 282     public void testDeadline1() throws Exception {
 283         Instant deadline = Instant.now().plusSeconds(2);
 284         Fiber<?> fiber;
 285         try (var scope = FiberScope.open(deadline)) {
 286             fiber = scope.schedule(() -> {
 287                 LockSupport.park();
 288                 assertTrue(Fiber.cancelled());
 289             });
 290         }
 291         fiber.join();
 292     }
 293 
 294     // fiber owner
 295     public void testDeadline2() throws Exception {
 296         runInFiber(() -> testDeadline1());
 297     }
 298 
 299     // thread owner, outer scope has deadline, schedule fiber in inner scope
 300     public void testDeadline3() {
 301         Instant deadline = Instant.now().plusSeconds(2);
 302         Fiber<?> fiber;
 303         try (var scope1 = FiberScope.open(deadline)) {
 304             try (var scope2 = FiberScope.open()) {
 305                 fiber = scope2.schedule(() -> {
 306                     LockSupport.park();
 307                     assertTrue(Fiber.cancelled());
 308                 });
 309             }
 310         }
 311         joinUninterruptibly(fiber);
 312         Thread.interrupted();
 313     }
 314 
 315     // fiber owner, outer scope has deadline, schedule fiber in inner scope
 316     public void testDeadline4() throws Exception {
 317         runInFiber(() -> testDeadline3());
 318     }
 319 
 320     // thread owner, outer scope has deadline, inner scope has later deadline
 321     public void testDeadline5() {
 322         Instant start = Instant.now();
 323         Instant deadline1 = start.plusSeconds(2);
 324         Instant deadline2 = start.plusSeconds(60);
 325         Fiber<?> fiber;
 326         try (var scope1 = FiberScope.open(deadline1)) {
 327             try (var scope2 = FiberScope.open(deadline2)) {
 328                 fiber = scope2.schedule(() -> {
 329                     LockSupport.park();
 330                     assertTrue(Fiber.cancelled());
 331                 });
 332             }
 333         }
 334         joinUninterruptibly(fiber);
 335         Thread.interrupted();
 336         long millis = Duration.between(start, Instant.now()).toMillis();
 337         assertTrue(millis >= 1900 && millis <= 5000, "Duration " + millis + "ms");
 338     }
 339 
 340     // fiber owner, outer scope has deadline, inner scope has later deadline
 341     public void testDeadline6() throws Exception {
 342         runInFiber(() -> testDeadline5());
 343     }
 344 
 345     // thread owner, outer scope has deadline, deeply nested inner scope has later deadline
 346     public void testDeadline7() {
 347         Instant start = Instant.now();
 348         Instant deadline1 = start.plusSeconds(2);
 349         Instant deadline2 = start.plusSeconds(60);
 350         Fiber<?> fiber;
 351         try (var scope1 = FiberScope.open(deadline1)) {
 352             try (var scope2 = FiberScope.open(/*do deadline*/)) {
 353                 try (var scope3 = FiberScope.open(deadline2)) {
 354                     fiber = scope3.schedule(() -> {
 355                         LockSupport.park();
 356                         assertTrue(Fiber.cancelled());
 357                     });
 358                 }
 359             }
 360         }
 361         joinUninterruptibly(fiber);
 362         Thread.interrupted();
 363         long millis = Duration.between(start, Instant.now()).toMillis();
 364         assertTrue(millis >= 1900 && millis <= 5000, "Duration " + millis + "ms");
 365     }
 366 
 367     // fiber owner, outer scope has deadline, deeply nested inner scope has later deadline
 368     public void testDeadline8() throws Exception {
 369         runInFiber(() -> testDeadline7());
 370     }
 371 
 372     // deadline in the past
 373     public void testDeadline9() throws Exception {
 374         Instant deadline = Instant.now().minusMillis(1);
 375         Fiber<?> fiber;
 376         try (var scope = FiberScope.open(deadline)) {
 377             fiber = scope.schedule(() -> {
 378                 LockSupport.park();
 379                 assertTrue(Fiber.cancelled());
 380             });
 381         }
 382         joinUninterruptibly(fiber);
 383         Thread.interrupted();
 384     }
 385 
 386     // thread owner, outer scope has deadline, schedule fiber in inner ignore-cancel scope
 387     public void testDeadline10() throws Exception {
 388         Instant start = Instant.now();
 389         Instant deadline = start.plusSeconds(1);
 390 
 391         Fiber<?> fiber;
 392         try (var scope1 = FiberScope.open(deadline)) {
 393             try (var scope2 = FiberScope.open(IGNORE_CANCEL)) {
 394                 fiber = scope2.schedule(() -> {
 395                     LockSupport.park();
 396                     assertFalse(Fiber.cancelled());
 397                 });
 398 
 399                 // unpark fiber after a delay
 400                 Unparker.schedule(fiber, Duration.ofSeconds(3));
 401             }
 402         }
 403         joinUninterruptibly(fiber);
 404         Thread.interrupted();
 405         long millis = Duration.between(start, Instant.now()).toMillis();
 406         assertTrue(millis >= 2900, "Duration " + millis + "ms");
 407     }
 408 
 409     // fiber owner, outer scope has deadline, schedule fiber in inner ignore-cancel scope
 410     public void testDeadline11() throws Exception {
 411         runInFiber(() -> testDeadline10());
 412     }
 413 
 414     // -- timeouts --
 415 
 416     // thread owner
 417     public void testTimeout1() throws Exception {
 418         Duration timeout = Duration.ofSeconds(2);
 419         Fiber<?> fiber;
 420         try (var scope = FiberScope.open(timeout)) {
 421             fiber = scope.schedule(() -> {
 422                 LockSupport.park();
 423                 assertTrue(Fiber.cancelled());
 424             });
 425         }
 426         fiber.join();
 427     }
 428 
 429     // fiber owner
 430     public void testTimeout2() throws Exception {
 431         runInFiber(() -> testTimeout1());
 432     }
 433 
 434     // zero timeout
 435     public void testTimeout3() throws Exception {
 436         Duration timeout = Duration.ofSeconds(0);
 437         Fiber<?> fiber;
 438         try (var scope = FiberScope.open(timeout)) {
 439             fiber = scope.schedule(() -> {
 440                 LockSupport.park();
 441                 assertTrue(Fiber.cancelled());
 442             });
 443         }
 444         joinUninterruptibly(fiber);
 445         Thread.interrupted();
 446     }
 447 
 448     public void testTimeout4() throws Exception {
 449         runInFiber(() -> testTimeout3());
 450     }
 451 
 452 
 453     // -- nulls and other exceptions --
 454 
 455     @Test(expectedExceptions = { NullPointerException.class })
 456     public void testNull1() {
 457         FiberScope.background().schedule((Runnable) null);
 458     }
 459 
 460     @Test(expectedExceptions = { NullPointerException.class })
 461     public void testNull2() {
 462         FiberScope.background().schedule((Callable<Void>) null);
 463     }
 464 
 465     @Test(expectedExceptions = { NullPointerException.class })
 466     public void testNull3() {
 467         Runnable task = () -> { };
 468         FiberScope.background().schedule((Executor)null, task);
 469     }
 470 
 471     @Test(expectedExceptions = { NullPointerException.class })
 472     public void testNull4() {
 473         Callable<String> task = () -> "foo";
 474         FiberScope.background().schedule((Executor)null, task);
 475     }
 476 
 477     @Test(expectedExceptions = { NullPointerException.class })
 478     public void testNull5() {
 479         ExecutorService scheduler = Executors.newCachedThreadPool();
 480         try {
 481             FiberScope.background().schedule(scheduler, (Runnable) null);
 482         } finally {
 483             scheduler.shutdown();
 484         }
 485     }
 486 
 487     @Test(expectedExceptions = { NullPointerException.class })
 488     public void testNull6() {
 489         ExecutorService scheduler = Executors.newCachedThreadPool();
 490         try {
 491             FiberScope.background().schedule(scheduler, (Callable<Void>) null);
 492         } finally {
 493             scheduler.shutdown();
 494         }
 495     }
 496 
 497     @Test(expectedExceptions = { NullPointerException.class })
 498     public void testBadOptions1() {
 499         FiberScope.open((Option) null);
 500     }
 501 
 502     @Test(expectedExceptions = { NullPointerException.class })
 503     public void testBadOptions2() {
 504         FiberScope.open(new Option[] { null });
 505     }
 506 
 507     @Test(expectedExceptions = { IllegalArgumentException.class })
 508     public void testBadOptions3() {
 509         FiberScope.open(IGNORE_CANCEL, CANCEL_AT_CLOSE);
 510     }
 511 
 512     @Test(expectedExceptions = { IllegalArgumentException.class })
 513     public void testBadOptions4() {
 514         FiberScope.open(IGNORE_CANCEL, PROPAGATE_CANCEL);
 515     }
 516 
 517 
 518     // -- supporting code --
 519 
 520     interface ThrowableTask {
 521         void run() throws Exception;
 522     }
 523 
 524     void runInFiber(ThrowableTask task) throws InterruptedException {
 525         var fiber = FiberScope.background().schedule(() -> {
 526             task.run();
 527             return null;
 528         });
 529         joinUninterruptibly(fiber);
 530         Thread.interrupted(); // clear interrupt
 531     }
 532 
 533     <V> V joinUninterruptibly(Fiber<V> fiber) {
 534         boolean interrupted = false;
 535         V result;
 536         while (true) {
 537             try {
 538                 result = fiber.join();
 539                 break;
 540             } catch (InterruptedException e) {
 541                 interrupted = true;
 542             }
 543         }
 544         if (interrupted) {
 545             Thread.currentThread().interrupt();
 546         }
 547         return result;
 548     }
 549 
 550     static class Unparker implements Runnable {
 551         final Fiber<?> fiber;
 552         final Duration delay;
 553 
 554         Unparker(Fiber<?> fiber, Duration delay) {
 555             this.fiber = fiber;
 556             this.delay = delay;
 557         }
 558 
 559         static void schedule(Fiber<?> fiber, Duration delay) {
 560             Unparker task  = new Unparker(fiber, delay);
 561             new Thread(task).start();
 562         }
 563 
 564         @Override
 565         public void run() {
 566             try {
 567                 Thread.sleep(delay.toMillis());
 568                 LockSupport.unpark(fiber);
 569             } catch (Exception e) {
 570                 e.printStackTrace();
 571             }
 572         }
 573     }
 574 }