1 /*
  2  * Copyright (c) 2021, 2022 Red Hat, Inc. 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 id=default
 26  * @summary Stress ScopedValue stack overflow recovery path
 27  * @enablePreview
 28  * @run main/othervm/timeout=300 StressStackOverflow
 29  */
 30 
 31 /*
 32  * @test id=no-TieredCompilation
 33  * @enablePreview
 34  * @run main/othervm/timeout=300 -XX:-TieredCompilation StressStackOverflow
 35  */
 36 
 37 /*
 38  * @test id=TieredStopAtLevel1
 39  * @enablePreview
 40  * @run main/othervm/timeout=300 -XX:TieredStopAtLevel=1 StressStackOverflow
 41  */
 42 
 43 /*
 44  * @test id=no-vmcontinuations
 45  * @requires vm.continuations
 46  * @enablePreview
 47  * @run main/othervm/timeout=300 -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations StressStackOverflow
 48  */
 49 
 50 import java.lang.ScopedValue.CallableOp;
 51 import java.time.Duration;
 52 import java.util.concurrent.ThreadLocalRandom;
 53 import java.util.concurrent.StructureViolationException;
 54 import java.util.concurrent.StructuredTaskScope;
 55 import java.util.function.Supplier;
 56 
 57 public class StressStackOverflow {
 58     public static final ScopedValue<Integer> el = ScopedValue.newInstance();
 59 
 60     public static final ScopedValue<Integer> inheritedValue = ScopedValue.newInstance();
 61 
 62     static final TestFailureException testFailureException = new TestFailureException("Unexpected value for ScopedValue");
 63     int ITERS = 1_000_000;
 64 
 65     static class TestFailureException extends RuntimeException {
 66         TestFailureException(String s) { super(s); }
 67     }
 68 
 69     static final long DURATION_IN_NANOS = Duration.ofMinutes(1).toNanos();
 70 
 71     // Test the ScopedValue recovery mechanism for stack overflows. We implement both CallableOp
 72     // and Runnable interfaces. Which one gets tested depends on the constructor argument.
 73     class DeepRecursion implements CallableOp<Object, RuntimeException>, Supplier<Object>, Runnable {
 74 
 75         enum Behaviour {
 76             CALL, RUN;
 77             private static final Behaviour[] values = values();
 78             public static Behaviour choose(ThreadLocalRandom tlr) {
 79                 return values[tlr.nextInt(3)];
 80             }
 81         }
 82 
 83         final Behaviour behaviour;
 84 
 85         public DeepRecursion(Behaviour behaviour) {
 86             this.behaviour = behaviour;
 87         }
 88 
 89         public void run() {
 90             final var last = el.get();
 91             while (ITERS-- > 0) {
 92                 if (System.nanoTime() - startTime > DURATION_IN_NANOS) {
 93                     return;
 94                 }
 95 
 96                 var nextRandomFloat = ThreadLocalRandom.current().nextFloat();
 97                 try {
 98                     switch (behaviour) {
 99                         case CALL -> ScopedValue.callWhere(el, el.get() + 1, () -> fibonacci_pad(20, this));
100                         case RUN -> ScopedValue.runWhere(el, el.get() + 1, () -> fibonacci_pad(20, this));
101                     }
102                     if (!last.equals(el.get())) {
103                         throw testFailureException;
104                     }
105                 } catch (StackOverflowError e) {
106                     if (nextRandomFloat <= 0.1) {
107                         ScopedValue.runWhere(el, el.get() + 1, this);
108                     }
109                 } catch (TestFailureException e) {
110                     throw e;
111                 } catch (Throwable throwable) {
112                     // StackOverflowErrors cause many different failures. These include
113                     // StructureViolationExceptions and InvocationTargetExceptions. This test
114                     // checks that, no matter what the failure mode, scoped values are handled
115                     // correctly.
116                 } finally {
117                     if (!last.equals(el.get())) {
118                         throw testFailureException;
119                     }
120                 }
121 
122                 Thread.yield();
123             }
124         }
125 
126         public Object get() {
127             run();
128             return null;
129         }
130 
131         public Object call() {
132             return get();
133         }
134     }
135 
136     static final Runnable nop = () -> {};
137 
138     // Consume some stack.
139     //
140     // The double recursion used here prevents an optimizing JIT from
141     // inlining all the recursive calls, which would make it
142     // ineffective.
143     private long fibonacci_pad1(int n, Runnable op) {
144         if (n <= 1) {
145             op.run();
146             return n;
147         }
148         return fibonacci_pad1(n - 1, op) + fibonacci_pad1(n - 2, nop);
149     }
150 
151     private static final Integer I_42 = 42;
152 
153     long fibonacci_pad(int n, Runnable op) {
154         final var last = el.get();
155         try {
156             return fibonacci_pad1(ThreadLocalRandom.current().nextInt(n), op);
157         } catch (StackOverflowError err) {
158             if (!inheritedValue.get().equals(I_42)) {
159                 throw testFailureException;
160             }
161             if (!last.equals(el.get())) {
162                 throw testFailureException;
163             }
164             throw err;
165         }
166     }
167 
168     // Run op in a new thread. Platform or virtual threads are chosen at random.
169     void runInNewThread(Runnable op) {
170         var threadFactory
171                 = (ThreadLocalRandom.current().nextBoolean() ? Thread.ofPlatform() : Thread.ofVirtual()).factory();
172         try (var scope = new StructuredTaskScope<>("", threadFactory)) {
173             var handle = scope.fork(() -> {
174                 op.run();
175                 return null;
176             });
177             scope.join();
178             handle.get();
179         } catch (TestFailureException e) {
180             throw e;
181         } catch (Exception e) {
182             throw new RuntimeException(e);
183         }
184     }
185 
186     public void run() {
187         try {
188             var carrier = ScopedValue.where(inheritedValue, 42).where(el, 0);
189             ScopedValue.runWhere(carrier, () -> {
190                 try (var scope = new StructuredTaskScope<>()) {
191                     try {
192                         if (ThreadLocalRandom.current().nextBoolean()) {
193                             // Repeatedly test Scoped Values set by ScopedValue::call(), get(), and run()
194                             final var deepRecursion
195                                 = new DeepRecursion(DeepRecursion.Behaviour.choose(ThreadLocalRandom.current()));
196                             deepRecursion.run();
197                         } else {
198                             // Recursively run ourself until we get a stack overflow
199                             // Catch the overflow and make sure the recovery path works
200                             // for values inherited from a StructuredTaskScope.
201                             Runnable op = new Runnable() {
202                                 public void run() {
203                                     try {
204                                         fibonacci_pad(20, this);
205                                     } catch (StackOverflowError e) {
206                                     } catch (TestFailureException e) {
207                                         throw e;
208                                     } catch (Throwable throwable) {
209                                         // StackOverflowErrors cause many different failures. These include
210                                         // StructureViolationExceptions and InvocationTargetExceptions. This test
211                                         // checks that, no matter what the failure mode, scoped values are handled
212                                         // correctly.
213                                     } finally {
214                                         if (!inheritedValue.get().equals(I_42)) {
215                                             throw testFailureException;
216                                         }
217                                     }
218                                 }
219                             };
220                             runInNewThread(op);
221                         }
222                         scope.join();
223                     } catch (StructureViolationException structureViolationException) {
224                         // Can happen if a stack overflow prevented a StackableScope from
225                         // being removed. We can continue.
226                     } catch (TestFailureException e) {
227                         throw e;
228                     } catch (Exception e) {
229                         throw new RuntimeException(e);
230                     }
231                 }
232             });
233         } catch (TestFailureException e) {
234             throw e;
235         } catch (Exception e) {
236             // Can happen if a stack overflow prevented a StackableScope from
237             // being removed. We can continue.
238         }
239     }
240 
241     static long startTime = System.nanoTime();
242 
243     public static void main(String[] args) {
244         var torture = new StressStackOverflow();
245         while (torture.ITERS > 0
246                 && System.nanoTime() - startTime <= DURATION_IN_NANOS) {
247             try {
248                 torture.run();
249                 if (inheritedValue.isBound()) {
250                     throw new TestFailureException("Should not be bound here");
251                 }
252             } catch (TestFailureException e) {
253                 throw e;
254             } catch (Exception e) {
255                 // ScopedValueContainer and StructuredTaskScope can
256                 // throw many exceptions on stack overflow. Ignore
257                 // them all.
258             }
259         }
260         System.out.println("OK");
261     }
262 }