changeset 53267:19f887f76d49 fibers

merge
author rpressler
date Fri, 04 Jan 2019 18:31:11 +0000
parents a72d48abd67e c92e170e24df
children b2a78e16910f c6c3a2d23acc
files
diffstat 11 files changed, 1785 insertions(+), 52 deletions(-) [+]
line wrap: on
line diff
--- a/src/java.base/share/classes/java/lang/Fiber.java	Fri Jan 04 18:28:19 2019 +0000
+++ b/src/java.base/share/classes/java/lang/Fiber.java	Fri Jan 04 18:31:11 2019 +0000
@@ -63,19 +63,22 @@
  * A <i>user mode</i> thread to execute a task that is scheduled by the Java
  * virtual machine rather than the operating system.
  *
- * <p> A {@code Fiber} is created and scheduled to execute a task by invoking
- * one of the {@link #schedule(Runnable) schedule} methods of this class. It
- * terminates when the task completes execution, either normally or with an
- * exception or error. The {@linkplain #awaitTermination() awaitTermination}
- * method can be used to wait for a fiber to terminate. The {@linkplain #join()
- * join} method also waits for a fiber to terminate, returning the result of the
- * task or throwing {@linkplain CompletionException} when the task terminates with
- * an exception. The {@linkplain #toFuture() toFuture} method can be used to
- * obtain a {@linkplain CompletableFuture} to interoperate with code that uses a
- * {@linkplain Future} or uses its cancellation semantics.
+ * <p> A {@code Fiber} is created and scheduled in a {@link FiberScope fiber
+ * scope} to execute a task by invoking one of the {@link #schedule(FiberScope, Runnable)
+ * schedule} methods of this class. A fiber terminates when the task completes
+ * execution, either normally or with a exception or error. The {@linkplain
+ * #awaitTermination() awaitTermination} method can be used to wait for a fiber
+ * to terminate. The {@linkplain #join() join} method also waits for a fiber to
+ * terminate, returning the result of the task or throwing {@linkplain
+ * CompletionException} when the task terminates with an exception. The {@linkplain
+ * #toFuture() toFuture} method can be used to obtain a {@linkplain CompletableFuture}
+ * to interoperate with code that uses a {@linkplain Future} or uses its
+ * cancellation semantics.
  *
  * <p> Unless otherwise noted, passing a {@code null} argument will cause a
  * {@linkplain NullPointerException} to be thrown.
+ *
+ * @param <V> the task result type
  */
 
 public class Fiber<V> {
@@ -88,6 +91,7 @@
     private static final VarHandle PARK_PERMIT;
     private static final VarHandle RESULT;
     private static final VarHandle FUTURE;
+    private static final VarHandle CANCELLED;
     static {
         try {
             MethodHandles.Lookup l = MethodHandles.lookup();
@@ -95,7 +99,8 @@
             PARK_PERMIT = l.findVarHandle(Fiber.class, "parkPermit", boolean.class);
             RESULT = l.findVarHandle(Fiber.class, "result", Object.class);
             FUTURE = l.findVarHandle(Fiber.class, "future", CompletableFuture.class);
-        } catch (Exception e) {
+            CANCELLED = l.findVarHandle(Fiber.class, "cancelled", boolean.class);
+       } catch (Exception e) {
             throw new InternalError(e);
         }
     }
@@ -108,6 +113,9 @@
     // carrier thread when mounted
     private volatile Thread carrierThread;
 
+    // current scope (read asynchronously by cancel)
+    private volatile FiberScope scope;
+
     // fiber state
     private static final short ST_NEW      = 0;
     private static final short ST_STARTED  = 1;
@@ -203,42 +211,58 @@
      * Creates and schedules a new {@code Fiber} to run the given task with the
      * default scheduler.
      *
+     * @apiNote
+     * For now, this method schedules the fiber in the DETACHED scope. This will
+     * be re-visited once there is more experience gained using fiber scopes.
+     *
      * @param task the task to execute
      * @return the fiber
      */
-    public static Fiber<Void> schedule(Runnable task) {
-        return new Fiber<Void>(DEFAULT_SCHEDULER, task).schedule();
+    public static Fiber<?> schedule(Runnable task) {
+        return new Fiber<>(DEFAULT_SCHEDULER, task).schedule(FiberScope.DETACHED);
     }
 
     /**
      * Creates and schedules a new {@code Fiber} to run the given task with the
      * given scheduler.
      *
+     * @apiNote
+     * For now, this method schedules the fiber in the DETACHED scope. This will
+     * be re-visited once there is more experience gained using fiber scopes.
+     *
      * @param scheduler the scheduler
      * @param task the task to execute
      * @return the fiber
      * @throws RejectedExecutionException if the scheduler cannot accept a task
      */
-    public static Fiber<Void> schedule(Executor scheduler, Runnable task) {
-        return new Fiber<Void>(scheduler, task).schedule();
+    public static Fiber<?> schedule(Executor scheduler, Runnable task) {
+        return new Fiber<>(scheduler, task).schedule(FiberScope.DETACHED);
     }
 
     /**
      * Creates and schedules a new {@code Fiber} to run the given value-returning
      * task. The {@code Fiber} is scheduled with the default scheduler.
      *
+     * @apiNote
+     * For now, this method schedules the fiber in the DETACHED scope. This will
+     * be re-visited once there is more experience gained using fiber scopes.
+     *
      * @param task the task to execute
      * @param <V> the task's result type
      * @return the fiber
      */
     public static <V> Fiber<V> schedule(Callable<? extends V> task) {
-        return new Fiber<V>(DEFAULT_SCHEDULER, task).schedule();
+        return new Fiber<V>(DEFAULT_SCHEDULER, task).schedule(FiberScope.DETACHED);
     }
 
     /**
      * Creates and schedules a new {@code Fiber} to run the given value-returning
      * task. The {@code Fiber} is scheduled with the given scheduler.
      *
+     * @apiNote
+     * For now, this method schedules the fiber in the DETACHED scope. This will
+     * be re-visited once there is more experience gained using fiber scopes.
+     *
      * @param scheduler the scheduler
      * @param task the task to execute
      * @param <V> the task's result type
@@ -246,7 +270,78 @@
      * @throws RejectedExecutionException if the scheduler cannot accept a task
      */
     public static <V> Fiber<V> schedule(Executor scheduler, Callable<? extends V> task) {
-        return new Fiber<V>(scheduler, task).schedule();
+        return new Fiber<V>(scheduler, task).schedule(FiberScope.DETACHED);
+    }
+
+    /**
+     * Creates and schedules a new {@code Fiber} in the given {@link FiberScope
+     * scope} to run the given task. The fiber is scheduled with the default
+     * scheduler.
+     *
+     * @param scope the fiber scope
+     * @param task the task to execute
+     * @return the fiber
+     * @throws IllegalCallerException if the caller thread or fiber is not
+     *         executing in the scope
+     */
+    public static Fiber<?> schedule(FiberScope scope, Runnable task) {
+        return new Fiber<>(DEFAULT_SCHEDULER, task).schedule(scope);
+    }
+
+    /**
+     * Creates and schedules a new {@code Fiber} to run the given task with the
+     * given scheduler given {@link FiberScope fiber scope}. The fiber is
+     * scheduled with the given scheduler.
+     *
+     * @param scope the fiber scope
+     * @param scheduler the scheduler
+     * @param task the task to execute
+     * @return the fiber
+     * @throws RejectedExecutionException if the scheduler cannot accept a task
+     * @throws IllegalCallerException if the caller thread or fiber is not
+     *         executing in the scope
+     */
+    public static Fiber<?> schedule(FiberScope scope,
+                                    Executor scheduler,
+                                    Runnable task) {
+        return new Fiber<>(scheduler, task).schedule(scope);
+    }
+
+    /**
+     * Creates and schedules a new {@code Fiber} in the given {@link FiberScope
+     * scope} to run the given value-returning task. The fiber is scheduled with
+     * the default scheduler.
+     *
+     * @param scope the fiber scope
+     * @param task the task to execute
+     * @param <V> the task's result type
+     * @return the fiber
+     * @throws IllegalCallerException if the caller thread or fiber is not
+     *         executing in the scope
+     */
+    public static <V> Fiber<V> schedule(FiberScope scope, Callable<? extends V> task) {
+        Objects.requireNonNull(scope);
+        return new Fiber<V>(DEFAULT_SCHEDULER, task).schedule(scope);
+    }
+
+    /**
+     * Creates and schedules a new {@code Fiber} in the given {@link FiberScope
+     * scope} to run the given value-returning task. The fiber is scheduled with
+     * the given scheduler.
+     *
+     * @param scope the fiber scope
+     * @param scheduler the scheduler
+     * @param task the task to execute
+     * @param <V> the task's result type
+     * @return the fiber
+     * @throws RejectedExecutionException if the scheduler cannot accept a task
+     * @throws IllegalCallerException if the caller thread or fiber is not
+     *         executing in the scope
+     */
+    public static <V> Fiber<V> schedule(FiberScope scope,
+                                        Executor scheduler,
+                                        Callable<? extends V> task) {
+        return new Fiber<V>(scheduler, task).schedule(scope);
     }
 
     /**
@@ -263,28 +358,51 @@
         return Thread.currentCarrierThread().getFiber();
     }
 
+    FiberScope scope() {
+        assert currentFiber() == this;
+        return scope;
+    }
+
+    void setScope(FiberScope scope) {
+        assert currentFiber() == this;
+        this.scope = scope;
+    }
+
     /**
      * Schedules this {@code Fiber} to execute.
      *
      * @return this fiber
      * @throws RejectedExecutionException if the scheduler cannot accept a task
      * @throws IllegalStateException if the fiber has already been scheduled
+     * @throws IllegalCallerException if the caller thread or fiber is not
+     *         executing in the scope
      */
-    private Fiber<V> schedule() {
+    private Fiber<V> schedule(FiberScope scope) {
+        Objects.requireNonNull(scope);
+
         if (!stateCompareAndSet(ST_NEW, ST_STARTED))
             throw new IllegalStateException("Fiber already scheduled");
 
-        Thread thread = Thread.currentCarrierThread();
-        Fiber<?> fiber = thread.getFiber();
+        this.scope = scope;
+        scope.onSchedule(this);
 
         // switch to carrier thread when submitting task. Revisit this when
         // ForkJoinPool is updated to reduce use of Thread.currentThread.
-        if (fiber != null) thread.setFiber(null);
+        Thread thread = Thread.currentCarrierThread();
+        Fiber<?> parentFiber = thread.getFiber();
+        if (parentFiber != null) thread.setFiber(null);
+        boolean scheduled = false;
         try {
             scheduler.execute(runContinuation);
+            scheduled = true;
         } finally {
-            if (fiber != null) thread.setFiber(fiber);
+            if (!scheduled) {
+                completeExceptionally(new IllegalStateException("stillborn"));
+                afterTerminate(false);
+            }
+            if (parentFiber != null) thread.setFiber(parentFiber);
         }
+
         return this;
     }
 
@@ -311,7 +429,7 @@
         } finally {
             unmount();
             if (cont.isDone()) {
-                afterTerminate();
+                afterTerminate(true);
             } else {
                 afterYield();
             }
@@ -375,19 +493,25 @@
     }
 
     /**
-     * Invoke after the continuation completes to set the state to ST_TERMINATED
+     * Invokes when the fiber terminates to set the state to ST_TERMINATED
      * and notify anyone waiting for the fiber to terminate.
+     *
+     * @param notifyAgents true to notify JVMTI agents
      */
-    private void afterTerminate() {
+    private void afterTerminate(boolean notifyAgents) {
+        assert result != null;
         int oldState = stateGetAndSet(ST_TERMINATED);
-        assert oldState == ST_RUNNABLE;
-        assert result != null;
+        assert oldState == ST_STARTED || oldState == ST_RUNNABLE;
 
-        if (notifyJvmtiEvents) {
+        if (notifyAgents && notifyJvmtiEvents) {
             Thread thread = Thread.currentCarrierThread();
             notifyFiberTerminated(thread, this);
         }
 
+        // notify scope so it can queue the fiber
+        assert scope != null;
+        scope.onTerminate(this);  // can fail with OOME
+
         // notify anyone waiting for this fiber to terminate
         signalTermination();
     }
@@ -620,7 +744,7 @@
      * interrupt status will be set. This will be re-visited once all the
      * details of cancellation are worked out.
      *
-     * @apiNote TBD if we need both await and join methods.
+     * @apiNote TBD if we need both awaitTermination and join methods.
      */
     public void awaitTermination() {
         boolean joinInterrupted = false;
@@ -729,36 +853,78 @@
     }
 
     /**
-     * Sets this fiber's cancel status. If the fiber hasn't terminated then it
-     * is also {@linkplain java.util.concurrent.locks.LockSupport#unpark(Object)
-     * unparked}.
+     * Sets this fiber's cancel status if not already set. If the fiber hasn't
+     * terminated then it is also {@link
+     * java.util.concurrent.locks.LockSupport#unpark(Object) unparked}.
      *
      * <p> This method has no effect on a {@linkplain CompletableFuture} obtained
-     * via the {@linkplain #toFuture()} method, its {@linkplain CompletableFuture#cancel(boolean)
-     * cancel(boolean)} method is not invoked.
+     * via the {@linkplain #toFuture()} method, its {@linkplain
+     * CompletableFuture#cancel(boolean) cancel(boolean)} method is not invoked.
      *
-     * @return this fiber
+     * @return {@code true} if the fiber's cancel status was set by this method
      * @throws RejectedExecutionException if using a scheduler and it cannot
      *         accept a task
      */
-    public Fiber<V> cancel() {
-        cancelled = true;
+    public boolean cancel() {
+        boolean changed = !cancelled && CANCELLED.compareAndSet(this, false, true);
         if (stateGet() != ST_TERMINATED) {
+            FiberScope scope = this.scope;
+            // scope is null before initially scheduled
+            if (scope != null) {
+                scope.onCancel(this);
+            }
             unpark();
         }
-        return this;
+        return changed;
+    }
+
+    /**
+     * Sets this fiber's cancel status if not already set and optionally unpark
+     * the fiber. Its FiberScope is not notified by this method.
+     */
+    void cancel(boolean unpark) {
+        if (!cancelled) {
+            CANCELLED.set(this, true);
+            if (unpark && stateGet() != ST_TERMINATED) {
+                unpark();
+            }
+        }
     }
 
     /**
      * Returns the fiber's cancel status.
      *
-     * @return {@code true} if the fiber has been cancelled
+     * @return {@code true} if the fiber's cancel status is set
      */
     public boolean isCancelled() {
         return cancelled;
     }
 
     /**
+     * Return {@code true} if the current fiber's cancel status is set and it
+     * is in executing in a {@link FiberScope#isCancellable() cancellable}
+     * scope. This method always returns {@code false} when invoked from a thread.
+     *
+     * @apiNote This method is intended to be used by blocking or compute bound
+     * operations that check cooperatively for cancellation.
+     *
+     * @return {@code true} if the current fiber has been cancelled
+     */
+    public static boolean cancelled() {
+        Fiber<?> fiber = currentFiber();
+        if (fiber != null && fiber.cancelled) {
+            FiberScope scope = fiber.scope;
+            if (scope != null) {
+                return scope.isCancellable();
+            } else {
+                // emulate the global scope for now
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * Waits for this fiber to terminate and returns the result of its task. If
      * the task completed with an exception then {@linkplain CompletionException}
      * is thrown with the exception as its cause.
@@ -768,7 +934,7 @@
      * interrupt status will be set. This will be re-visited once all the
      * details of cancellation are worked out.
      *
-     * @apiNote TBD if we need both await and join methods.
+     * @apiNote TBD if we need both awaitTermination and join methods.
      *
      * @return the result or {@code null} if the fiber was created with a Runnable
      * @throws CompletionException if the task completed with an exception
@@ -959,7 +1125,11 @@
                 sb.append(g.getName());
             }
         } else {
-            sb.append("<no carrier thread>");
+            if (stateGet() == ST_TERMINATED) {
+                sb.append("<terminated>");
+            } else {
+                sb.append("<no carrier thread>");
+            }
         }
         sb.append("]");
         return sb.toString();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.base/share/classes/java/lang/FiberScope.java	Fri Jan 04 18:31:11 2019 +0000
@@ -0,0 +1,749 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package java.lang;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.BlockingSource;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Stream;
+
+import jdk.internal.misc.Strands;
+
+/**
+ * A scope in which {@link Fiber}s are scheduled with support for cancellation,
+ * deadlines, and reaping of terminated fibers.
+ *
+ * <p> A {@code FiberScope} is created and <em>entered</em> by invoking one of
+ * the static methods defined by this class. It is <em>exited</em> by invoking
+ * its {@linkplain #close() close} method. A scope can only be exited when all
+ * fibers scheduled in the scope have terminated and thus the {@code close}
+ * method blocks until all fibers scheduled in the scope have terminated.
+ * As a special case, fibers can be scheduled in the {@linkplain #DETACHED
+ * DETACHED} scope for cases where fibers are <em>unmanaged</em> or need to
+ * <em>outlive</em> the thread or fiber that scheduled them.
+ *
+ * <p> {@code FiberScope} implements {@linkplain AutoCloseable} so that the
+ * try-with-resources statement can be used to ensure that a scope is exited.
+ * The following example schedules two fibers in a scope. The try-with-resources
+ * statement completes when the block completes and both fibers scheduled in the
+ * scope terminate.
+ *
+ * <pre>{@code
+ *     Fiber<?> fiber1, fiber2;
+ *     try (var scope = FiberScope.cancellable()) {
+ *         fiber1 = Fiber.schedule(scope, () -> { ... });
+ *         fiber1 = Fiber.schedule(scope, () -> { ... });
+ *     });
+ *     assertFalse(fiber1.isAlive());
+ *     assertFalse(fiber2.isAlive());
+ * }</pre>
+ *
+ * <p> Fiber scopes support cancellation. Fibers test for cancellation by invoking
+ * the {@linkplain Fiber#cancelled()} method. If a {@code Fiber} executing in
+ * a scope is cancelled then all fibers scheduled in the scope are cancelled.
+ * As a special case for cleanup and recovery operations, the {@linkplain
+ * #notCancellable} method creates and enters a scope that <em>shields</em> a
+ * fiber from cancellation. {@code Fiber.cancelled()} always returns {@code false}
+ * when running in a <em>non-cancelable</em> scope.
+ *
+ * <p> Fiber scopes support deadlines and timeouts. The {@linkplain
+ * #withDeadline(Instant) withDeadline} method enters a fiber scope that cancels
+ * the fiber in the scope, and all fibers scheduled in the scope, when the deadline
+ * expires. The {@linkplain #withTimeout(Duration) withTimeout} is similar for
+ * cases where a timeout is used.
+ *
+ * <p> Fiber scopes may be nested. A thread or fiber executing in a scope may
+ * enter another nested scope. Any fibers scheduled in the inner scope must
+ * terminate to allow the thread or fiber exit back to the outer scope. Cancellation
+ * propagates when cancellable scopes are nested. Fiber scopes using deadlines
+ * and timeouts may also be nested.
+ *
+ * <p> A fiber scope has a {@linkplain #terminationQueue() terminationQueue}.
+ * Fibers scheduled in a scope are queued when they terminate so that the owner
+ * (usually) can obtain the result of the task that the fiber executed.
+ * The termination queue can be disabled with the {@linkplain #disableTerminationQueue()
+ * disableTerminationQueue} method for cases where results are not needed or
+ * where fibers communicate by other means.
+ *
+ * <p> Unless otherwise noted, passing a {@code null} argument will cause a
+ * {@linkplain NullPointerException} to be thrown.
+ *
+ * @apiNote
+ * The following example is a method that schedules a fiber for each task
+ * specified to the method. It returns the result of the first task that completes,
+ * cancelling and waiting for any outstanding fibers to terminate before it
+ * returns.
+ *
+ * <pre>{@code
+ *     <V> V anyOf(Callable<? extends V>[] tasks) throws Throwable {
+ *         try (var scope = FiberScope.cancellable()) {
+ *             Arrays.stream(tasks).forEach(task -> Fiber.schedule(scope, task));
+ *             try {
+ *                 return (V) scope.terminationQueue().take().join();
+ *             } catch (CompletionException e) {
+ *                 throw e.getCause();
+ *             } finally {
+ *                 // cancel any fibers that are still running
+ *                 scope.fibers().forEach(Fiber::cancel);
+ *             }
+ *         }
+ *     }
+ * }</pre>
+ * An alternative for this example would be to use {@link Fiber#toFuture()} and
+ * {@link java.util.concurrent.CompletableFuture#anyOf CompletableFuture.anyOf(...)}
+ * to wait for one of the tasks to complete.
+ *
+ * <p> The following extends this example to return the result of the first
+ * <em>successful</em> task. If no task succeeds then it throws the exception
+ * from the first unsuccessful task to complete. This method creates and enters
+ * the scope with a deadline so that all fibers are cancelled if the deadline
+ * expires before a result is returned.
+ *
+ * <pre>{@code
+ *     <V> V anySuccessful(Callable<? extends V>[] tasks, Instant deadline) throws Throwable {
+ *         try (var scope = FiberScope.withDeadline(deadline)) {
+ *             Arrays.stream(tasks).forEach(task -> Fiber.schedule(scope, task));
+ *             Throwable firstException = null;
+ *             while (scope.hasRemaining()) {
+ *                 try {
+ *                     V result = (V) scope.terminationQueue().take().join();
+ *                     // cancel any fibers that are still running
+ *                     scope.fibers().forEach(Fiber::cancel);
+ *                     return result;
+ *                 } catch (CompletionException e) {
+ *                     if (firstException == null) {
+ *                         firstException = e.getCause();
+ *                     }
+ *                 }
+ *             }
+ *             throw firstException;
+ *         }
+ *     }
+ * }</pre>
+ *
+ * <p> The following is a more complicated example. The method is called with
+ * an array of socket addresses and returns a SocketChannel connected to one
+ * of the addresses. The connection attempts are staggered. A fiber is scheduled
+ * to connect to the first socket address and if the connection is not
+ * established within a certain time then another fiber is scheduled to try to
+ * connect to the next socket address. The staggered attempts continue until a
+ * connection is established, none of the connections succeed, or the deadline
+ * is reached. In the event that several connections are established then all
+ * but one are closed so that the method does not leak resources.
+ * <pre>{@code
+ *     SocketChannel connectAny(SocketAddress[] addresses,
+ *                              Instant deadline,
+ *                              Duration staggerInterval) throws Exception {
+ *         assert addresses.length > 0;
+ *
+ *         SocketChannel channel = null;
+ *         Exception exception = null;
+ *
+ *         try (var scope = FiberScope.withDeadline(deadline)) {
+ *
+ *             // schedule a fiber to connect to the first address
+ *             Fiber.schedule(scope, () -> SocketChannel.open(addresses[0]));
+ *
+ *             int next = 1; // index of next address to try
+ *
+ *             var realDeadline = FiberScope.currentDeadline().orElseThrow();
+ *             Duration waitTime = staggerInterval;
+ *
+ *             while (scope.hasRemaining()) {
+ *
+ *                 // wait for a timeout or a fiber to terminate
+ *                 Fiber<?> fiber = scope.terminationQueue().poll(waitTime);
+ *                 if (fiber != null) {
+ *                     try {
+ *                         SocketChannel ch = (SocketChannel) fiber.join();
+ *                         if (channel == null) {
+ *                             // first successful connection, cancel other attempts
+ *                             channel = ch;
+ *                             scope.fibers().forEach(Fiber::cancel);
+ *                         } else {
+ *                             // another connection is established, it's not needed
+ *                             ch.close();
+ *                         }
+ *                     } catch (CompletionException e) {
+ *                         // connect failed, remember first exception
+ *                         if (channel == null && exception == null) {
+ *                             exception = (Exception) e.getCause();
+ *                         }
+ *                     }
+ *                 }
+ *
+ *                 // if no connection has been established, the deadline hasn't been
+ *                 // reached, and there are further addresses to try, then schedule
+ *                 // a fiber to connect to the next address
+ *                 boolean expired = realDeadline.compareTo(Instant.now()) <= 0;
+ *                 if (channel == null && !expired && next < addresses.length) {
+ *                     var address = addresses[next++];
+ *                     Fiber.schedule(scope, () -> SocketChannel.open(address));
+ *                 }
+ *
+ *                 // if the deadline has been reached or there are no more addresses
+ *                 // to try then bump the waiting time to avoid needless wakeup
+ *                 if (expired || next >= addresses.length) {
+ *                     waitTime = Duration.ofSeconds(Long.MAX_VALUE);
+ *                 }
+ *             }
+ *         }
+ *
+ *         assert channel != null || exception != null;
+ *         if (channel != null) {
+ *             return channel;
+ *         } else {
+ *             throw exception;
+ *         }
+ *     }
+ * }</pre>
+ */
+
+public class FiberScope implements AutoCloseable {
+    FiberScope() { }
+
+    /**
+     * The <em>detached</em> scope. Fibers scheduled in the detached scope can
+     * be {@link Fiber#cancel() cancelled}.
+     * The detached scope does not support a termination queue, {@link #hasRemaining()
+     * hasRemaining} always returns {@code false}, and the {@link #fibers()
+     * fibers} methods always returns an empty stream. This scope cannot be exited,
+     * the {@link #close() close} method always fails.
+     */
+    public static final FiberScope DETACHED = new DetachedFiberScope();
+
+    /**
+     * Creates and enters a <em>cancellable</em> scope. The current {@link
+     * Thread#currentThread() thread} or {@link Fiber#current() fiber} is the
+     * <em>owner</em> of the scope, only the owner can exit the scope with the
+     * {@linkplain #close() close} method. If the owner is a fiber and it is
+     * {@link Fiber#cancel() cancelled} then all fibers scheduled in the
+     * scope are also cancelled.
+     *
+     * @return a <em>cancellable</em> scope.
+     */
+    public static FiberScope cancellable() {
+        return new FiberScopeImpl(true, null);
+    }
+
+    /**
+     * Creates and enters a <em>non-cancellable</em> scope. The current {@link
+     * Thread#currentThread() thread} or {@link Fiber#current() fiber} is the
+     * <em>owner</em> of the scope, only the owner can exit the scope with the
+     * {@linkplain #close() close} method. If the owner is a fiber and it is
+     * {@link Fiber#cancel() cancelled} then the cancellation is not propagated
+     * to fibers scheduled in the scope. Non-cancellable scopes are intended for
+     * cleanup and recovery operations that need to be shielded from cancellation.
+     *
+     * @return a <em>non-cancellable</em> scope.
+     */
+    public static FiberScope notCancellable() {
+        return new FiberScopeImpl(false, null);
+    }
+
+    /**
+     * Creates and enters a <em>cancellable</em> scope. The current {@link
+     * Thread#currentThread() thread} or {@link Fiber#current() fiber} is the
+     * <em>owner</em> of the scope, only the owner can exit the scope with the
+     * {@linkplain #close() close} method. If the deadline is reached before the
+     * scope is exited then all fibers scheduled in the scope are cancelled,
+     * along with the owner when it is a fiber.
+     *
+     * @param deadline the deadline
+     * @return a <em>cancellable</em> scope that cancels fibers when the deadline
+     *         is reached
+     */
+    public static FiberScope withDeadline(Instant deadline) {
+        return new FiberScopeImpl(true, Objects.requireNonNull(deadline));
+    }
+
+    /**
+     * Creates and enters a <em>cancellable</em> scope. The current {@link
+     * Thread#currentThread() thread} or {@link Fiber#current() fiber} is the
+     * <em>owner</em> of the scope, only the owner can exit the scope with the
+     * {@linkplain #close() close} method. If the timeout expires before the
+     * scope is exited then all fibers scheduled in the scope are cancelled,
+     * along with the owner when it is a fiber.
+     *
+     * @param timeout the timeout
+     * @return a <em>cancellable</em> scope that cancels fibers when the timeout
+     *         expires
+     */
+    public static FiberScope withTimeout(Duration timeout) {
+        return withDeadline(Instant.now().plus(timeout));
+    }
+
+    /**
+     * Exits this scope. This method waits until all fibers scheduled in the
+     * scope have terminated. If the {@link #currentDeadline() current deadline}
+     * has expired then {@code CancellationException} is thrown after all fibers
+     * scheduled in the scope have terminated.
+     *
+     * <p> If this scope is already closed then invoking this method has no
+     * effect.
+     *
+     * @throws IllegalCallerException if not called from the thread or fiber
+     *         that created and entered the scope
+     * @throws CancellationException if the deadline has expired
+     */
+    @Override
+    public void close() {
+        throw new RuntimeException("not implemented");
+    }
+
+    /**
+     * Returns a {@code Stream} of the fibers scheduled in this scope that are
+     * still {@link Fiber#isAlive() alive}. This method returns an empty
+     * stream for the {@linkplain #DETACHED DETACHED} scope.
+     *
+     * @return a stream of the active fibers in the scope
+     */
+    public Stream<Fiber<?>> fibers() {
+        throw new RuntimeException("not implemented");
+    }
+
+    /**
+     * Returns the queue to retrieve fibers, scheduled in this scope, when they
+     * terminate.
+     *
+     * @return the termination queue
+     * @throws UnsupportedOperationException if this scope is the DETACHED scope.
+     * @see #disableTerminationQueue()
+     */
+    public BlockingSource<Fiber<?>> terminationQueue() {
+        throw new RuntimeException("not implemented");
+    }
+
+    /**
+     * Disables further queuing of fibers, scheduled in this scope, when they
+     * terminate.
+     *
+     * @return this scope
+     * @see #terminationQueue()
+     */
+    public FiberScope disableTerminationQueue() {
+        throw new RuntimeException("not implemented");
+    }
+
+    /**
+     * Returns {@code true} if there there are any fibers scheduled in this scope
+     * that have been not been retrieved from the {@link #terminationQueue()
+     * termination queue}. If this is the {@linkplain #DETACHED DETACHED} scope
+     * then this method returns {@code false}.
+     *
+     * @return {@code true} if there are fibers scheduled in this scope that
+     *         have not been retrieved from the termination queue
+     */
+    public boolean hasRemaining() {
+        throw new RuntimeException("not implemented");
+    }
+
+    /**
+     * Returns the <em>current deadline</em>, if any. The current deadline may
+     * be the deadline of an enclosing scope. Enclosing scopes up to, and
+     * excluding, the closest enclosing non-cancellable scope are considered.
+     *
+     * @return the current deadline or an empty {@code Optional} if there is no
+     *         deadline
+     */
+    public static Optional<Instant> currentDeadline() {
+        Object me = Strands.currentStrand();
+        FiberScope scope;
+        if (me instanceof Thread) {
+            scope = ((Thread) me).scope();
+        } else {
+            scope = ((Fiber) me).scope();
+        }
+        Instant deadline = null;
+        while ((scope instanceof FiberScopeImpl) && scope.isCancellable()) {
+            Instant d = ((FiberScopeImpl) scope).deadline();
+            if (d != null && (deadline == null || deadline.compareTo(d) > 0)) {
+                deadline = d;
+            }
+            scope = scope.previous();
+        }
+        return Optional.ofNullable(deadline);
+    }
+
+    /**
+     * Returns the previous scope when nested.
+     */
+    FiberScope previous() {
+        throw new RuntimeException("not implemented");
+    }
+
+    /**
+     * Returns true if this scope supports cancellation.
+     */
+    boolean isCancellable() {
+        throw new RuntimeException("not implemented");
+    }
+
+    /**
+     * Invoked when a fiber is scheduled to add the fiber to this scope
+     */
+    void onSchedule(Fiber<?> child) { }
+
+    /**
+     * Invoked when a fiber terminates
+     */
+    void onTerminate(Fiber<?> fiber) { }
+
+    /**
+     * Invoked when a fiber is cancelled
+     */
+    void onCancel(Fiber<?> fiber) { }
+}
+
+class DetachedFiberScope extends FiberScope {
+    @Override
+    public void close() {
+        throw new IllegalCallerException("not the owner");
+    }
+    @Override
+    public Stream<Fiber<?>> fibers() {
+        return Stream.empty();
+    }
+    @Override
+    public BlockingSource<Fiber<?>> terminationQueue() {
+        throw new UnsupportedOperationException();
+    }
+    @Override
+    public FiberScope disableTerminationQueue() {
+        return this;
+    }
+    @Override
+    public boolean hasRemaining() {
+        return false;
+    }
+    @Override
+    FiberScope previous() {
+        return null;
+    }
+    @Override
+    boolean isCancellable() {
+        return true;
+    }
+}
+
+class FiberScopeImpl extends FiberScope {
+    private final boolean cancellable;
+    private final Instant deadline;
+    private final Object owner;
+    private final FiberScope previous;
+
+    // the active set of fibers scheduled in the scope
+    private final Set<Fiber<?>> fibers;
+
+    // cancellation and deadline support
+    private volatile boolean cancelled;
+    private volatile boolean expired;
+    private final Future<?> canceller;
+
+    // termination queue
+    private final BlockingQueue<Fiber<?>> queue;
+    private final BlockingSource<Fiber<?>> publicQueue;
+    private volatile boolean disableTerminationQueue;
+
+    // close/exit support
+    private volatile boolean closed;
+    private final ReentrantLock closeLock;
+    private final Condition closeCondition;
+
+    FiberScopeImpl(boolean cancellable, Instant deadline) {
+        Object owner = Strands.currentStrand();
+
+        this.cancellable = cancellable;
+        this.deadline = deadline;
+        this.owner = owner;
+        this.fibers = ConcurrentHashMap.newKeySet();
+
+        if (deadline != null) {
+            Duration timeout = Duration.between(Instant.now(), deadline);
+            if (timeout.isZero() || timeout.isNegative()) {
+                // deadline has already expired
+                this.cancelled = true;
+                this.expired = true;
+                this.canceller = null;
+                if (owner instanceof Fiber) {
+                    ((Fiber<?>) owner).cancel(/*unpark*/false);
+                }
+            } else {
+                // schedule timer task
+                long nanos = TimeUnit.NANOSECONDS.convert(timeout);
+                this.canceller = timeoutScheduler.schedule(this::deadlineExpired,
+                                                           nanos,
+                                                           TimeUnit.NANOSECONDS);
+            }
+        } else {
+            this.canceller = null;
+        }
+
+        if (owner instanceof Thread) {
+            Thread thread = (Thread) owner;
+            this.previous = thread.scope();
+            thread.setScope(this);
+        } else {
+            Fiber<?> fiber = (Fiber<?>) owner;
+            this.previous = fiber.scope();
+            fiber.setScope(this);
+
+            // invoked by fiber with its cancel status set
+            if (cancellable && fiber.isCancelled()) {
+                this.cancelled = true;
+            }
+        }
+
+        this.queue = new LinkedBlockingQueue<>();
+        this.publicQueue = new TerminationQueue(queue);
+
+        this.closeLock = new ReentrantLock();
+        this.closeCondition = closeLock.newCondition();
+    }
+
+    /**
+     * Invoked by the timer task when deadlines expires
+     */
+    private void deadlineExpired() {
+        cancelled = true;
+        expired = true;
+        fibers.forEach(Fiber::cancel);
+        if (owner instanceof Fiber) {
+            ((Fiber<?>) owner).cancel(/*unpark*/true);
+        }
+    }
+
+    @Override
+    public void close() {
+        if (Strands.currentStrand() != owner)
+            throw new IllegalCallerException();
+        if (closed)
+            return;
+        closed = true;
+
+        try {
+
+            // wait for all fibers to terminate
+            closeLock.lock();
+            try {
+                while (!fibers.isEmpty()) {
+                    closeCondition.awaitUninterruptibly();
+                }
+            } finally {
+                closeLock.unlock();
+            }
+
+            // deadline expired
+            if (expired) {
+                throw new CancellationException("Deadline expired");
+            } else if (canceller != null) {
+                // cancel timer task
+                canceller.cancel(false);
+            }
+
+        } finally {
+            // restore to previous scope
+            if (owner instanceof Thread) {
+                Thread thread = (Thread) owner;
+                if (thread.scope() != this)
+                    throw new InternalError();
+                thread.setScope(previous);
+            } else {
+                Fiber<?> fiber = (Fiber<?>) owner;
+                if (fiber.scope() != this)
+                    throw new InternalError();
+                fiber.setScope(previous);
+
+                // propagate cancellation to previous scope
+                if (fiber.isCancelled() && previous != null) {
+                    previous.onCancel(fiber);
+                }
+            }
+        }
+    }
+
+    @Override
+    public Stream<Fiber<?>> fibers() {
+        return fibers.stream();
+    }
+
+    @Override
+    public BlockingSource<Fiber<?>> terminationQueue() {
+        return publicQueue;
+    }
+
+    @Override
+    public FiberScope disableTerminationQueue() {
+        disableTerminationQueue = true;
+        return this;
+    }
+
+    @Override
+    public boolean hasRemaining() {
+        if (queue.isEmpty() && fibers.isEmpty()) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    @Override
+    FiberScope previous() {
+        return previous;
+    }
+
+    @Override
+    boolean isCancellable() {
+        return cancellable;
+    }
+
+    Instant deadline() {
+        return deadline;
+    }
+
+    @Override
+    void onSchedule(Fiber<?> child) {
+        // check called from a thread or fiber in the scope
+        Object me = Strands.currentStrand();
+        FiberScope scope;
+        if (me instanceof Thread) {
+            scope = ((Thread) me).scope();
+            if (scope == null) {
+                throw new IllegalCallerException("caller not in fiber scope");
+            }
+        } else {
+            scope = ((Fiber) me).scope();
+            assert scope != null;
+        }
+
+        while (scope != this) {
+            scope = scope.previous();
+            if (scope == null) {
+                throw new IllegalCallerException("caller not in fiber scope");
+            }
+        }
+
+        // add to set; okay to do this when the scope is closed as the current
+        // thread or fiber is in the scope
+        fibers.add(child);
+
+        // forward cancel status to child if scope has been cancelled.
+        if (cancelled) {
+            child.cancel(/*unpark*/false);
+        }
+    }
+
+    @Override
+    void onTerminate(Fiber<?> fiber) {
+        // queue terminated fiber before removing from fibers set to avoid
+        // hasRemaining seeing them both as empty
+        if (!disableTerminationQueue) {
+            boolean interrupted = false;
+            boolean done = false;
+            while (!done) {
+                try {
+                    queue.put(fiber);
+                    done = true;
+                } catch (InterruptedException e) {
+                    interrupted = true;
+                }
+            }
+            if (interrupted) {
+                Thread.currentThread().interrupt();
+            }
+        }
+
+        boolean removed = fibers.remove(fiber);
+        assert removed;
+
+        // notify owner if waiting to exit scope
+        if (closed && fibers.isEmpty()) {
+            closeLock.lock();
+            try {
+                closeCondition.signalAll();
+            } finally {
+                closeLock.unlock();
+            }
+        }
+    }
+
+    @Override
+    void onCancel(Fiber<?> fiber) {
+        if ((fiber == owner) && cancellable) {
+            cancelled = true;
+            fibers.forEach(Fiber::cancel);
+        }
+    }
+
+    private static final ScheduledExecutorService timeoutScheduler = timeoutScheduler();
+    private static ScheduledExecutorService timeoutScheduler() {
+        ScheduledThreadPoolExecutor stpe = (ScheduledThreadPoolExecutor)
+            Executors.newScheduledThreadPool(1, r ->
+                    AccessController.doPrivileged(new PrivilegedAction<>() {
+                        public Thread run() {
+                            Thread t = new Thread(r);
+                            t.setDaemon(true);
+                            return t;
+                        }}));
+        stpe.setRemoveOnCancelPolicy(true);
+        return stpe;
+    }
+
+    /**
+     * Wraps a BlockingQueue to avoid handing out reference to internal queue
+     */
+    private static class TerminationQueue implements BlockingSource<Fiber<?>> {
+        private final BlockingQueue<Fiber<?>> queue;
+        TerminationQueue(BlockingQueue<Fiber<?>> queue) {
+            this.queue = queue;
+        }
+        public Fiber<?> take() throws InterruptedException {
+            return queue.take();
+        }
+        public Fiber<?> poll() {
+            return queue.poll();
+        }
+        public Fiber<?> poll(Duration duration) throws InterruptedException {
+            long nanos = TimeUnit.NANOSECONDS.convert(duration);
+            return queue.poll(nanos, TimeUnit.NANOSECONDS);
+        }
+    }
+}
\ No newline at end of file
--- a/src/java.base/share/classes/java/lang/Thread.java	Fri Jan 04 18:28:19 2019 +0000
+++ b/src/java.base/share/classes/java/lang/Thread.java	Fri Jan 04 18:31:11 2019 +0000
@@ -33,6 +33,7 @@
 import java.security.PrivilegedAction;
 import java.util.Map;
 import java.util.HashMap;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.TimeUnit;
@@ -294,6 +295,9 @@
     @HotSpotIntrinsicCandidate
     private static native Thread currentThread0();
 
+    private Fiber<?> fiber;
+    private FiberScope scope;
+
     /**
      * Binds this thread to given Fiber. Once set, Thread.currentThread() will
      * return the Fiber rather than the Thread object for the carrier thread.
@@ -311,7 +315,15 @@
         return fiber;
     }
 
-    private Fiber<?> fiber;
+    FiberScope scope() {
+        assert Thread.currentThread() == this;
+        return scope;
+    }
+
+    void setScope(FiberScope scope) {
+        assert Thread.currentThread() == this;
+        this.scope = scope;
+    }
 
     /**
      * A hint to the scheduler that the current thread is willing to yield
@@ -352,20 +364,29 @@
         if (millis < 0) {
             throw new IllegalArgumentException("timeout value is negative");
         }
-        Fiber<?> f = currentCarrierThread().getFiber();
-        if (f != null) {
+        Fiber<?> fiber = currentCarrierThread().getFiber();
+        if (fiber != null) {
             if (!Fiber.emulateCurrentThread()) {
                 throw new UnsupportedOperationException(
                     "Thread.sleep cannot be used in the context of a fiber");
             }
+
+            Thread shadowThread = fiber.shadowThreadOrNull();
+            if (Fiber.cancelled()) {
+                getAndClearInterrupt(shadowThread);
+                throw new InterruptedException();
+            }
+
             long nanos = TimeUnit.NANOSECONDS.convert(millis, TimeUnit.MILLISECONDS);
             do {
                 long startTime = System.nanoTime();
                 Fiber.parkNanos(nanos);
-                // check if park interrupted by Thread.interrupt
-                Thread t = f.shadowThreadOrNull();
-                if (t != null && t.getAndClearInterrupt())
+
+                // check if park interrupted by Thread.interrupt or cancel
+                if (getAndClearInterrupt(shadowThread) || Fiber.cancelled()) {
                     throw new InterruptedException();
+                }
+
                 nanos -= System.nanoTime() - startTime;
             } while (nanos > 0);
         } else {
@@ -376,6 +397,7 @@
 
     private static native void sleep0(long millis) throws InterruptedException;
 
+
     /**
      * Causes the currently executing thread to sleep (temporarily cease
      * execution) for the specified number of milliseconds plus the specified
@@ -1165,6 +1187,14 @@
         return isInterrupted(true);
     }
 
+    private static boolean getAndClearInterrupt(Thread thread) {
+        if (thread != null) {
+            return thread.getAndClearInterrupt();
+        } else {
+            return false;
+        }
+    }
+
     /**
      * Tests if some Thread has been interrupted.  The interrupted state
      * is reset or not based on the value of clearInterrupted that is
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.base/share/classes/java/util/concurrent/BlockingSource.java	Fri Jan 04 18:31:11 2019 +0000
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package java.util.concurrent;
+
+import java.time.Duration;
+
+/**
+ * A source of elements that are retrieved by methods that block when there
+ * are no elements present.
+ *
+ * <p> BlockingSource implementations should be thread-safe.
+ *
+ * @apiNote If this interface is useful then we could retrofit BlockingQueue,
+ * CompletionService, java.nio.file.WatchService, and maybe others to extend it.
+ *
+ * @param <E> the type of elements
+ */
+public interface BlockingSource<E> {
+
+    /**
+     * Retrieves the next element, waiting if none are yet present.
+     *
+     * @return the next element
+     * @throws InterruptedException if interrupted while waiting
+     */
+    E take() throws InterruptedException;
+
+    /**
+     * Retrieves the next element or {@code null} if none are present.
+     *
+     * @return the next element or {@code null} if none are present
+     */
+    E poll();
+
+    /**
+     * Retrieves the next element, waiting if necessary up to the specified wait
+     * time if none are yet present.
+     *
+     * @param timeout how long to wait before giving up
+     * @return the next element {@code null} if the specified waiting time
+     *         elapses before one is present
+     * @throws InterruptedException if interrupted while waiting
+     */
+    E poll(Duration timeout) throws InterruptedException;
+}
--- a/src/java.base/share/classes/java/util/concurrent/SynchronousQueue.java	Fri Jan 04 18:28:19 2019 +0000
+++ b/src/java.base/share/classes/java/util/concurrent/SynchronousQueue.java	Fri Jan 04 18:31:11 2019 +0000
@@ -446,7 +446,7 @@
                 ? (timed ? MAX_TIMED_SPINS : MAX_UNTIMED_SPINS)
                 : 0;
             for (;;) {
-                if (t != null && t.isInterrupted())
+                if ((t != null && t.isInterrupted()) || Fiber.cancelled())
                     s.tryCancel();
                 SNode m = s.match;
                 if (m != null)
@@ -751,7 +751,7 @@
                 ? (timed ? MAX_TIMED_SPINS : MAX_UNTIMED_SPINS)
                 : 0;
             for (;;) {
-                if (t != null && t.isInterrupted())
+                if ((t != null && t.isInterrupted()) || Fiber.cancelled())
                     s.tryCancel(e);
                 Object x = s.item;
                 if (x != e)
--- a/src/java.base/share/classes/sun/nio/ch/SelChImpl.java	Fri Jan 04 18:28:19 2019 +0000
+++ b/src/java.base/share/classes/sun/nio/ch/SelChImpl.java	Fri Jan 04 18:31:11 2019 +0000
@@ -87,11 +87,19 @@
             Poller.register(strand, getFDVal(), event);
             if (isOpen()) {
                 try {
+                    if (Fiber.cancelled()) {
+                        // throw IOException for now, will be replaced later with close
+                        throw new IOException("I/O operation cancelled");
+                    }
                     if (nanos == 0) {
                         Strands.parkFiber();
                     } else {
                         Strands.parkFiber(nanos);
                     }
+                    if (Fiber.cancelled()) {
+                        // throw IOException for now, will be replaced later with close
+                        throw new IOException("I/O operation cancelled");
+                    }
                 } finally {
                     Poller.deregister(strand, getFDVal(), event);
                 }
--- a/src/java.base/share/classes/sun/nio/ch/SocketStreams.java	Fri Jan 04 18:28:19 2019 +0000
+++ b/src/java.base/share/classes/sun/nio/ch/SocketStreams.java	Fri Jan 04 18:31:11 2019 +0000
@@ -111,11 +111,19 @@
             Poller.register(strand, fdVal, event);
             if (isOpen()) {
                 try {
+                    if (Fiber.cancelled()) {
+                        // throw IOException for now
+                        throw new SocketException("I/O operation cancelled");
+                    }
                     if (nanos == 0) {
                         Strands.parkFiber();
                     } else {
                         Strands.parkFiber(nanos);
                     }
+                    if (Fiber.cancelled()) {
+                        // throw IOException for now
+                        throw new SocketException("I/O operation cancelled");
+                    }
                 } finally{
                     Poller.deregister(strand, fdVal, event);
                 }
--- a/test/jdk/java/lang/Fiber/Basic.java	Fri Jan 04 18:28:19 2019 +0000
+++ b/test/jdk/java/lang/Fiber/Basic.java	Fri Jan 04 18:31:11 2019 +0000
@@ -829,13 +829,13 @@
     @Test(expectedExceptions = { NullPointerException.class })
     public void testNull3() {
         Runnable task = () -> { };
-        Fiber.schedule(null, task);
+        Fiber.schedule((Executor)null, task);
     }
 
     @Test(expectedExceptions = { NullPointerException.class })
     public void testNull4() {
         Callable<String> task = () -> "foo";
-        Fiber.schedule(null, task);
+        Fiber.schedule((Executor)null, task);
     }
 
     @Test(expectedExceptions = { NullPointerException.class })
--- a/test/jdk/java/lang/Fiber/NetSockets.java	Fri Jan 04 18:28:19 2019 +0000
+++ b/test/jdk/java/lang/Fiber/NetSockets.java	Fri Jan 04 18:31:11 2019 +0000
@@ -148,6 +148,43 @@
         });
     }
 
+    /**
+     * Cancel a fiber blocked in read
+     */
+    public void testSocketReadCancel() {
+        test(() -> {
+            try (var connection = new Connection()) {
+                var fiber = Fiber.current().orElseThrow();
+                ScheduledCanceller.schedule(fiber, DELAY);
+                Socket s = connection.socket1();
+                try {
+                    int n = s.getInputStream().read();
+                    throw new RuntimeException("read returned " + n);
+                } catch (SocketException expected) { }
+            }
+        });
+    }
+
+    /**
+     * Cancel a fiber blocked in write
+     */
+    public void testSocketWriteCancel() {
+        test(() -> {
+            try (var connection = new Connection()) {
+                var fiber = Fiber.current().orElseThrow();
+                ScheduledCanceller.schedule(fiber, DELAY);
+                Socket s = connection.socket1();
+                try {
+                    byte[] ba = new byte[100*10024];
+                    OutputStream out = s.getOutputStream();
+                    for (;;) {
+                        out.write(ba);
+                    }
+                } catch (SocketException expected) { }
+            }
+        });
+    }
+
 
     // -- supporting classes --
 
@@ -264,4 +301,29 @@
             new Thread(new ScheduledWriter(s, ba, delay)).start();
         }
     }
+
+    /**
+     * Cancel a fiber after a delay
+     */
+    static class ScheduledCanceller implements Runnable {
+        private final Fiber fiber;
+        private final long delay;
+
+        ScheduledCanceller(Fiber fiber, long delay) {
+            this.fiber = fiber;
+            this.delay = delay;
+        }
+
+        @Override
+        public void run() {
+            try {
+                Thread.sleep(delay);
+                fiber.cancel();
+            } catch (Exception e) { }
+        }
+
+        static void schedule(Fiber fiber, long delay) {
+            new Thread(new ScheduledCanceller(fiber, delay)).start();
+        }
+    }
 }
--- a/test/jdk/java/lang/Fiber/NioChannels.java	Fri Jan 04 18:28:19 2019 +0000
+++ b/test/jdk/java/lang/Fiber/NioChannels.java	Fri Jan 04 18:31:11 2019 +0000
@@ -160,6 +160,23 @@
     }
 
     /**
+     * Fiber cancelled while blocked in SocketChannel.read
+     */
+    public void testSocketChannelReadCancel() {
+        test(() -> {
+            try (var connection = new Connection()) {
+                SocketChannel sc = connection.channel1();
+                var fiber = Fiber.current().orElseThrow();
+                ScheduledCanceller.schedule(fiber, DELAY);
+                try {
+                    int n = sc.read(ByteBuffer.allocate(100));
+                    throw new RuntimeException("read returned " + n);
+                } catch (IOException expected) { }
+            }
+        });
+    }
+
+    /**
      * SocketChannel close while Fiber blocked in write
      */
     public void testSocketChannelWriteAsyncClose() {
@@ -180,7 +197,7 @@
     }
 
     /**
-     * Fiber interrupted while blocked in SocketChannel.read
+     * Fiber interrupted while blocked in SocketChannel.write
      */
     public void testSocketChannelWriteInterrupt() {
         test(() -> {
@@ -200,6 +217,27 @@
     }
 
     /**
+     * Fiber cancelled while blocked in SocketChannel.write
+     */
+    public void testSocketChannelWritCeancel() {
+        test(() -> {
+            try (var connection = new Connection()) {
+                SocketChannel sc = connection.channel1();
+                var fiber = Fiber.current().orElseThrow();
+                ScheduledCanceller.schedule(fiber, DELAY);
+                try {
+                    ByteBuffer bb = ByteBuffer.allocate(100*10024);
+                    for (;;) {
+                        int n = sc.write(bb);
+                        assertTrue(n > 0);
+                        bb.clear();
+                    }
+                } catch (IOException expected) { }
+            }
+        });
+    }
+
+    /**
      * ServerSocketChannel accept, no blocking
      */
     public void testServerSocketChannelAccept1() {
@@ -269,6 +307,25 @@
     }
 
     /**
+     * Fiber cancelled while blocked in ServerSocketChannel.accept
+     */
+    public void testServerSocketChannelAcceptCancel() {
+        test(() -> {
+            try (var ssc = ServerSocketChannel.open()) {
+                InetAddress lh = InetAddress.getLocalHost();
+                ssc.bind(new InetSocketAddress(lh, 0));
+                var fiber = Fiber.current().orElseThrow();
+                ScheduledCanceller.schedule(fiber, DELAY);
+                try {
+                    SocketChannel sc = ssc.accept();
+                    sc.close();
+                    throw new RuntimeException("connection accepted???");
+                } catch (IOException expected) { }
+            }
+        });
+    }
+
+    /**
      * DatagramChannel receive/send, no blocking
      */
     public void testDatagramhannelSendReceive1() {
@@ -350,6 +407,24 @@
     }
 
     /**
+     * Fiber cancelled while blocked in DatagramChannel.receive
+     */
+    public void testDatagramhannelReceiveCancel() {
+        test(() -> {
+            try (DatagramChannel dc = DatagramChannel.open()) {
+                InetAddress lh = InetAddress.getLocalHost();
+                dc.bind(new InetSocketAddress(lh, 0));
+                var fiber = Fiber.current().orElseThrow();
+                ScheduledCanceller.schedule(fiber, DELAY);
+                try {
+                    dc.receive(ByteBuffer.allocate(100));
+                    throw new RuntimeException("receive returned");
+                } catch (IOException expected) { }
+            }
+        });
+    }
+
+    /**
      * Pipe read/write, no blocking
      */
     public void testPipeReadWrite1() {
@@ -450,6 +525,23 @@
     }
 
     /**
+     * Fiber cancelled while blocked in Pipe.SourceChannel read
+     */
+    public void testPipeReadCancel() {
+        test(() -> {
+            Pipe p = Pipe.open();
+            try (Pipe.SourceChannel source = p.source()) {
+                var fiber = Fiber.current().orElseThrow();
+                ScheduledCanceller.schedule(fiber, DELAY);
+                try {
+                    int n = source.read(ByteBuffer.allocate(100));
+                    throw new RuntimeException("read returned " + n);
+                } catch (IOException expected) { }
+            }
+        });
+    }
+
+    /**
      * Pipe.SinkChannel close while Fiber blocked in write
      */
     public void testPipeWriteAsyncClose() {
@@ -489,6 +581,26 @@
         });
     }
 
+    /**
+     * Fiber cancelled while blocked in Pipe.SinkChannel write
+     */
+    public void testPipeWriteCancel() {
+        test(() -> {
+            Pipe p = Pipe.open();
+            try (Pipe.SinkChannel sink = p.sink()) {
+                var fiber = Fiber.current().orElseThrow();
+                ScheduledCanceller.schedule(fiber, DELAY);
+                try {
+                    ByteBuffer bb = ByteBuffer.allocate(100*10024);
+                    for (;;) {
+                        int n = sink.write(bb);
+                        assertTrue(n > 0);
+                        bb.clear();
+                    }
+                } catch (IOException expected) { }
+            }
+        });
+    }
 
     // -- supporting classes --
 
@@ -568,6 +680,31 @@
     }
 
     /**
+     * Cancel a fiber after a delay
+     */
+    static class ScheduledCanceller implements Runnable {
+        private final Fiber fiber;
+        private final long delay;
+
+        ScheduledCanceller(Fiber fiber, long delay) {
+            this.fiber = fiber;
+            this.delay = delay;
+        }
+
+        @Override
+        public void run() {
+            try {
+                Thread.sleep(delay);
+                fiber.cancel();
+            } catch (Exception e) { }
+        }
+
+        static void schedule(Fiber fiber, long delay) {
+            new Thread(new ScheduledCanceller(fiber, delay)).start();
+        }
+    }
+
+    /**
      * Establish a connection to a socket address after a delay
      */
     static class ScheduledConnector implements Runnable {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/lang/Fiber/Scopes.java	Fri Jan 04 18:31:11 2019 +0000
@@ -0,0 +1,502 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * @test
+ * @run testng Scopes
+ * @summary Basic tests for java.lang.FiberScope
+ */
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.stream.Stream;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.*;
+import java.util.concurrent.locks.*;
+
+import org.testng.annotations.Test;
+import org.testng.annotations.DataProvider;
+import static org.testng.Assert.*;
+
+@Test
+public class Scopes {
+
+    // -- detached scope --
+
+    // fibers scheduled in the detached scope can be cancelled
+    public void testDetached1() {
+        runInFiber(FiberScope.DETACHED, () -> {
+            assertFalse(Fiber.cancelled());
+            Fiber.current().map(Fiber::cancel);
+            assertTrue(Fiber.cancelled());
+        });
+    }
+
+    // the detached scope does not support a termination queue
+    @Test(expectedExceptions = { UnsupportedOperationException.class })
+    public void testDetached2() {
+        FiberScope.DETACHED.terminationQueue();
+    }
+
+    // The hasRemaining method always returns false for the detached scope
+    public void testDetached3() {
+        FiberScope scope = FiberScope.DETACHED;
+        assertFalse(scope.hasRemaining());
+        var fiber = Fiber.schedule(scope, () -> LockSupport.park());
+        try {
+            assertFalse(scope.hasRemaining());
+        } finally {
+            LockSupport.unpark(fiber);
+        }
+    }
+
+    // The fibers method always returns an empty stream for the detached scope
+    public void testDetached4() {
+        FiberScope scope = FiberScope.DETACHED;
+        assertEmpty(scope.fibers());
+        var fiber = Fiber.schedule(scope, () -> LockSupport.park());
+        try {
+            assertEmpty(scope.fibers());
+        } finally {
+            LockSupport.unpark(fiber);
+        }
+    }
+
+    // the detached scope cannot be exited
+    @Test(expectedExceptions = { IllegalCallerException.class })
+    public void testDetached5() {
+        FiberScope.DETACHED.close();
+    }
+
+    // -- cancellable scope --
+
+    // close waits until all fibers scheduled in a scope to terminate
+    public void testCancellable1() {
+        var ref = new AtomicReference<Fiber<?>>();
+        try (var scope = FiberScope.cancellable()) {
+            var fiber = Fiber.schedule(scope, () -> {
+                Thread.sleep(2000);
+                return null;
+            });
+            ref.set(fiber);
+        }
+        Fiber<?> fiber = ref.get();
+        assertFalse(fiber.isAlive());
+    }
+
+    // cancel a fiber in a cancellable scope
+    public void testCancellable2() {
+        runInFiber(() -> {
+            try (var scope = FiberScope.cancellable()) {
+                assertFalse(Fiber.cancelled());
+                Fiber.current().map(Fiber::cancel);
+                assertTrue(Fiber.cancelled());
+            }
+        });
+    }
+
+    // cancel a fiber in a cancellable scope propagates cancellation
+    public void testCancellable3() {
+        runInFiber(() -> {
+            try (var scope = FiberScope.cancellable()) {
+                var child = Fiber.schedule(scope, () -> {
+                    try {
+                        Thread.sleep(60*1000);
+                        assertTrue(false);
+                    } catch (InterruptedException e) {
+                        assertTrue(Fiber.cancelled());
+                    }
+                    return null;
+                });
+                Fiber.current().map(Fiber::cancel);
+                child.join();
+            }
+        });
+    }
+
+    // FiberScope::fibers includes an element for running fibers
+    public void testCancellable4() {
+        try (var scope = FiberScope.cancellable()) {
+            assertEmpty(scope.fibers());
+            var fiber = Fiber.schedule(scope, () -> LockSupport.park());
+            try {
+                assertTrue(scope.fibers().filter(f -> f == fiber).findAny().isPresent());
+            } finally {
+                LockSupport.unpark(fiber);
+                fiber.join();
+            }
+            assertEmpty(scope.fibers());
+        }
+    }
+
+    // test termination queue in cancellable scope
+    public void testCancellable5() throws Exception {
+        try (var scope = FiberScope.cancellable()) {
+            var queue = scope.terminationQueue();
+            assertFalse(scope.hasRemaining());
+            assertTrue(queue.poll() == null);
+            var child = Fiber.schedule(scope, () -> { });
+            assertTrue(scope.hasRemaining());
+            var fiber = queue.take();
+            assertTrue(fiber == child);
+            assertFalse(fiber.isAlive());
+            assertFalse(scope.hasRemaining());
+            assertTrue(queue.poll() == null);
+        }
+    }
+
+
+    // -- non-cancellable scope --
+
+    // close waits until all fibers scheduled in a scope to terminate
+    public void testNonCancellable1() {
+        var ref = new AtomicReference<Fiber<?>>();
+        try (var scope = FiberScope.notCancellable()) {
+            var fiber = Fiber.schedule(scope, () -> {
+                Thread.sleep(2000);
+                return null;
+            });
+            ref.set(fiber);
+        }
+        Fiber<?> fiber = ref.get();
+        assertFalse(fiber.isAlive());
+    }
+
+    // cancel a fiber in a non-cancellable scope
+    public void testNonCancellable2() {
+        runInFiber(() -> {
+            try (var scope = FiberScope.notCancellable()) {
+                assertFalse(Fiber.cancelled());
+                Fiber.current().map(Fiber::cancel);
+                assertFalse(Fiber.cancelled());
+            }
+        });
+    }
+
+    // cancel a fiber in a non-cancellable scope
+    public void testNonCancellable3() {
+        runInFiber(() -> {
+            try (var scope = FiberScope.notCancellable()) {
+                var child = Fiber.schedule(scope, () -> {
+                    Thread.sleep(1000);
+                    assertFalse(Fiber.cancelled());
+                    return null;
+                });
+                Fiber.current().map(Fiber::cancel);
+                assertFalse(Fiber.cancelled());
+                child.join();
+            }
+        });
+    }
+
+    // FiberScope::fibers includes an element for running fibers
+    public void testNonCancellable4() {
+        try (var scope = FiberScope.notCancellable()) {
+            assertEmpty(scope.fibers());
+            var fiber = Fiber.schedule(scope, () -> LockSupport.park());
+            try {
+                assertTrue(scope.fibers().filter(f -> f == fiber).findAny().isPresent());
+            } finally {
+                LockSupport.unpark(fiber);
+                fiber.join();
+            }
+            assertEmpty(scope.fibers());
+        }
+    }
+
+    // test termination queue in non-cancellable scope
+    public void testNonCancellable5() throws Exception {
+        try (var scope = FiberScope.cancellable()) {
+            var queue = scope.terminationQueue();
+            assertFalse(scope.hasRemaining());
+            assertTrue(queue.poll() == null);
+            var child = Fiber.schedule(scope, () -> { });
+            assertTrue(scope.hasRemaining());
+            var fiber = queue.take();
+            assertTrue(fiber == child);
+            assertFalse(fiber.isAlive());
+            assertFalse(scope.hasRemaining());
+            assertTrue(queue.poll() == null);
+        }
+    }
+
+
+    // -- withDeadline --
+
+    // close waits until all fibers scheduled in a scope to terminate
+    public void testWithDeadline1() {
+        var ref = new AtomicReference<Fiber<?>>();
+        var deadline = Instant.now().plusSeconds(60);
+        try (var scope = FiberScope.withDeadline(deadline)) {
+            assertTrue(FiberScope.currentDeadline().orElseThrow().equals(deadline));
+            var fiber = Fiber.schedule(scope, () -> {
+                Thread.sleep(2000);
+                return null;
+            });
+            ref.set(fiber);
+        }
+        Fiber<?> fiber = ref.get();
+        assertFalse(fiber.isAlive());
+    }
+
+    // cancel a fiber in a cancellable scope
+    public void testWithDeadline2() {
+        runInFiber(() -> {
+            var deadline = Instant.now().plusSeconds(60);
+            try (var scope = FiberScope.withDeadline(deadline)) {
+                assertTrue(FiberScope.currentDeadline().orElseThrow().equals(deadline));
+                assertFalse(Fiber.cancelled());
+                Fiber.current().map(Fiber::cancel);
+                assertTrue(Fiber.cancelled());
+            }
+        });
+    }
+
+    // cancel a fiber in a cancellable scope propagates cancellation
+    public void testWithDeadline3() {
+        runInFiber(() -> {
+            var deadline = Instant.now().plusSeconds(60);
+            try (var scope = FiberScope.withDeadline(deadline)) {
+                assertTrue(FiberScope.currentDeadline().orElseThrow().equals(deadline));
+                var child = Fiber.schedule(scope, () -> {
+                    try {
+                        Thread.sleep(60*1000);
+                        assertTrue(false);
+                    } catch (InterruptedException e) {
+                        assertTrue(Fiber.cancelled());
+                    }
+                    return null;
+                });
+                Fiber.current().map(Fiber::cancel);
+                child.join();
+            }
+        });
+    }
+
+    // FiberScope::fibers includes an element for running fibers
+    public void testWithDeadline4() {
+        var deadline = Instant.now().plusSeconds(60);
+        try (var scope = FiberScope.withDeadline(deadline)) {
+            assertTrue(FiberScope.currentDeadline().orElseThrow().equals(deadline));
+            assertEmpty(scope.fibers());
+            var fiber = Fiber.schedule(scope, () -> LockSupport.park());
+            try {
+                assertTrue(scope.fibers().filter(f -> f == fiber).findAny().isPresent());
+            } finally {
+                LockSupport.unpark(fiber);
+                fiber.join();
+            }
+            assertEmpty(scope.fibers());
+        }
+    }
+
+    // test termination queue in cancellable scope
+    public void testWithDeadline5() throws Exception {
+        var deadline = Instant.now().plusSeconds(60);
+        try (var scope = FiberScope.withDeadline(deadline)) {
+            assertTrue(FiberScope.currentDeadline().orElseThrow().equals(deadline));
+            var queue = scope.terminationQueue();
+            assertFalse(scope.hasRemaining());
+            assertTrue(queue.poll() == null);
+            var child = Fiber.schedule(scope, () -> { });
+            assertTrue(scope.hasRemaining());
+            var fiber = queue.take();
+            assertTrue(fiber == child);
+            assertFalse(fiber.isAlive());
+            assertFalse(scope.hasRemaining());
+            assertTrue(queue.poll() == null);
+        }
+    }
+
+    // deadline expires
+    @Test(expectedExceptions = { CancellationException.class })
+    public void testWithDeadline6() {
+        var deadline = Instant.now().plusSeconds(1);
+        try (var scope = FiberScope.withDeadline(deadline)) {
+            Fiber.schedule(scope, () -> {
+                Thread.sleep(60 * 1000);
+                return null;
+            });
+        }
+    }
+
+    // deadline in the past
+    @Test(expectedExceptions = { CancellationException.class })
+    public void testWithDeadline7() {
+        var deadline = Instant.now().minusSeconds(1);
+        try (var scope = FiberScope.withDeadline(deadline)) { }
+    }
+
+    // deadline in the past
+    @Test(expectedExceptions = { CancellationException.class })
+    public void testWithDeadline8() {
+        var deadline = Instant.now().minusSeconds(1);
+        try (var scope = FiberScope.withDeadline(deadline)) {
+            try {
+                Fiber.schedule(() -> assertTrue(Fiber.cancelled())).join();
+                assertTrue(false);
+            } catch (CompletionException expected) { }
+        }
+    }
+
+
+    // -- withTimeout --
+
+    // TBD
+
+
+    // -- nesting and cancellation propagtion --
+
+    // exiting to an outer scope when cancelled should cancel fibers in the outer scope
+    public void testNesting1() {
+        runInFiber(() -> {
+            try (var outer = FiberScope.cancellable()) {
+                var child = Fiber.schedule(outer, () -> LockSupport.park());
+                try (var inner = FiberScope.cancellable()) {
+                    assertFalse(child.isCancelled());
+                    Fiber.current().map(Fiber::cancel);
+                    assertFalse(child.isCancelled());
+                }
+                assertTrue(child.isCancelled());
+            }
+        });
+    }
+
+
+    // currentDeadline with nested scopes
+    public void testNesting2() {
+        runInFiber(() -> {
+            Instant deadline = Instant.now().plusSeconds(500);
+            try (var outer = FiberScope.withDeadline(deadline)) {
+                assertTrue(FiberScope.currentDeadline().orElseThrow().equals(deadline));
+                Instant closerDeadline = deadline.minusSeconds(1);
+                try (var inner = FiberScope.withDeadline(closerDeadline)) {
+                    assertTrue(FiberScope.currentDeadline().orElseThrow().equals(closerDeadline));
+                }
+                assertTrue(FiberScope.currentDeadline().orElseThrow().equals(deadline));
+            }
+        });
+    }
+
+    public void testPropagate1() {
+        runInFiber(() -> {
+            try (var outer = FiberScope.cancellable()) {
+                var ref = new AtomicReference<Fiber<?>>();
+                Fiber<?> child = Fiber.schedule(outer, () -> {
+                    try (var inner = FiberScope.cancellable()) {
+                        ref.set(Fiber.schedule(inner, () -> LockSupport.park()));
+                    }
+                });
+                Fiber<?> grandchild = waitForValue(ref);
+
+                // cancel self
+                Fiber.current().map(Fiber::cancel);
+
+                // cancel status on both child and grandchild should be set
+                try {
+                    assertTrue(child.isCancelled());
+                    assertTrue(grandchild.isCancelled());
+                } finally {
+                    LockSupport.unpark(grandchild);
+                }
+            }
+        });
+    }
+
+    public void testPropagate2() {
+        runInFiber(() -> {
+            try (var outer = FiberScope.cancellable()) {
+                var ref = new AtomicReference<Fiber<?>>();
+                Fiber<?> child = Fiber.schedule(outer, () -> {
+                    try (var inner = FiberScope.notCancellable()) {
+                        ref.set(Fiber.schedule(inner, () -> LockSupport.park()));
+                    }
+                });
+                Fiber<?> grandchild = waitForValue(ref);
+
+                // cancel self
+                Fiber.current().map(Fiber::cancel);
+
+                // cancel status on child should be set
+                try {
+                    assertTrue(child.isCancelled());
+                    assertFalse(grandchild.isCancelled());
+                } finally {
+                    LockSupport.unpark(grandchild);
+                }
+            }
+        });
+    }
+
+
+    // -- nulls --
+
+    @Test(expectedExceptions = { NullPointerException.class })
+    public void testNull1() {
+        FiberScope.withDeadline(null);
+    }
+
+    @Test(expectedExceptions = { NullPointerException.class })
+    public void testNull2() {
+        FiberScope.withTimeout(null);
+    }
+
+    @Test(expectedExceptions = { NullPointerException.class })
+    public void testNull3() throws Exception {
+        try (var scope = FiberScope.cancellable()) {
+            scope.terminationQueue().poll(null);
+        }
+    }
+
+
+    // -- supporting code --
+
+    void runInFiber(Runnable task) {
+        Fiber.schedule(task).join();
+    }
+
+
+    void runInFiber(FiberScope scope, Runnable task) {
+        Fiber.schedule(scope, task).join();
+    }
+
+    <T> T waitForValue(AtomicReference<T> ref) {
+        T obj;
+        boolean interrupted = false;
+        while ((obj = ref.get()) == null) {
+            try {
+                Thread.sleep(20);
+            } catch (InterruptedException e) {
+                interrupted = true;
+            }
+        }
+        if (interrupted)
+            Thread.currentThread().interrupt();
+        return obj;
+    }
+
+    static void assertEmpty(Stream<?> stream) {
+        assertFalse(stream.findAny().isPresent());
+    }
+
+}