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.concurrent.StructuredTaskScope.Joiner;
 56 import java.util.function.Supplier;
 57 
 58 public class StressStackOverflow {
 59     public static final ScopedValue<Integer> el = ScopedValue.newInstance();
 60 
 61     public static final ScopedValue<Integer> inheritedValue = ScopedValue.newInstance();
 62 
 63     static final TestFailureException testFailureException = new TestFailureException("Unexpected value for ScopedValue");
 64     int ITERS = 1_000_000;
 65 
 66     static class TestFailureException extends RuntimeException {
 67         TestFailureException(String s) { super(s); }
 68     }
 69 
 70     static final long DURATION_IN_NANOS = Duration.ofMinutes(1).toNanos();
 71 
 72     // Test the ScopedValue recovery mechanism for stack overflows. We implement both CallableOp
 73     // and Runnable interfaces. Which one gets tested depends on the constructor argument.
 74     class DeepRecursion implements CallableOp<Object, RuntimeException>, Supplier<Object>, Runnable {
 75 
 76         enum Behaviour {
 77             CALL, RUN;
 78             private static final Behaviour[] values = values();
 79             public static Behaviour choose(ThreadLocalRandom tlr) {
 80                 return values[tlr.nextInt(3)];
 81             }
 82         }
 83 
 84         final Behaviour behaviour;
 85 
 86         public DeepRecursion(Behaviour behaviour) {
 87             this.behaviour = behaviour;
 88         }
 89 
 90         public void run() {
 91             final var last = el.get();
 92             while (ITERS-- > 0) {
 93                 if (System.nanoTime() - startTime > DURATION_IN_NANOS) {
 94                     return;
 95                 }
 96 
 97                 var nextRandomFloat = ThreadLocalRandom.current().nextFloat();
 98                 try {
 99                     switch (behaviour) {
100                         case CALL -> ScopedValue.where(el, el.get() + 1).call(() -> fibonacci_pad(20, this));
101                         case RUN -> ScopedValue.where(el, el.get() + 1).run(() -> fibonacci_pad(20, this));
102                     }
103                     if (!last.equals(el.get())) {
104                         throw testFailureException;
105                     }
106                 } catch (StackOverflowError e) {
107                     if (nextRandomFloat <= 0.1) {
108                         ScopedValue.where(el, el.get() + 1).run(this);
109                     }
110                 } catch (TestFailureException e) {
111                     throw e;
112                 } catch (Throwable throwable) {
113                     // StackOverflowErrors cause many different failures. These include
114                     // StructureViolationExceptions and InvocationTargetExceptions. This test
115                     // checks that, no matter what the failure mode, scoped values are handled
116                     // correctly.
117                 } finally {
118                     if (!last.equals(el.get())) {
119                         throw testFailureException;
120                     }
121                 }
122 
123                 Thread.yield();
124             }
125         }
126 
127         public Object get() {
128             run();
129             return null;
130         }
131 
132         public Object call() {
133             return get();
134         }
135     }
136 
137     static final Runnable nop = () -> {};
138 
139     // Consume some stack.
140     //
141     // The double recursion used here prevents an optimizing JIT from
142     // inlining all the recursive calls, which would make it
143     // ineffective.
144     private long fibonacci_pad1(int n, Runnable op) {
145         if (n <= 1) {
146             op.run();
147             return n;
148         }
149         return fibonacci_pad1(n - 1, op) + fibonacci_pad1(n - 2, nop);
150     }
151 
152     private static final Integer I_42 = 42;
153 
154     long fibonacci_pad(int n, Runnable op) {
155         final var last = el.get();
156         try {
157             return fibonacci_pad1(ThreadLocalRandom.current().nextInt(n), op);
158         } catch (StackOverflowError err) {
159             if (!inheritedValue.get().equals(I_42)) {
160                 throw testFailureException;
161             }
162             if (!last.equals(el.get())) {
163                 throw testFailureException;
164             }
165             throw err;
166         }
167     }
168 
169     // Run op in a new thread. Platform or virtual threads are chosen at random.
170     void runInNewThread(Runnable op) {
171         var threadFactory
172                 = (ThreadLocalRandom.current().nextBoolean() ? Thread.ofPlatform() : Thread.ofVirtual()).factory();
173         try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withThreadFactory(threadFactory))) {
174             var handle = scope.fork(() -> {
175                 op.run();
176                 return null;
177             });
178             scope.join();
179             handle.get();
180         } catch (TestFailureException e) {
181             throw e;
182         } catch (Exception e) {
183             throw new RuntimeException(e);
184         }
185     }
186 
187     public void run() {
188         try {
189             ScopedValue.where(inheritedValue, 42).where(el, 0).run(() -> {
190                 try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
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 }