1 /*
  2  * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 /*
 25  * @test
 26  * @modules java.base/jdk.internal.ref
 27  *          jdk.incubator.foreign/jdk.incubator.foreign
 28  * @run testng/othervm TestResourceScope
 29  */
 30 
 31 import java.lang.ref.Cleaner;
 32 
 33 import jdk.incubator.foreign.ResourceScope;
 34 import jdk.internal.ref.CleanerFactory;
 35 
 36 import org.testng.annotations.DataProvider;
 37 import org.testng.annotations.Test;
 38 import static org.testng.Assert.*;
 39 
 40 import java.util.ArrayList;
 41 import java.util.List;
 42 import java.util.Set;
 43 import java.util.concurrent.atomic.AtomicInteger;
 44 import java.util.concurrent.atomic.AtomicReference;
 45 import java.util.function.Supplier;
 46 import java.util.stream.IntStream;
 47 
 48 public class TestResourceScope {
 49 
 50     final static int N_THREADS = 100;
 51 
 52     @Test(dataProvider = "cleaners")
 53     public void testConfined(Supplier<Cleaner> cleanerSupplier) {
 54         AtomicInteger acc = new AtomicInteger();
 55         Cleaner cleaner = cleanerSupplier.get();
 56         ResourceScope scope = cleaner != null ?
 57                 ResourceScope.newConfinedScope(cleaner) :
 58                 ResourceScope.newConfinedScope();
 59         for (int i = 0 ; i < N_THREADS ; i++) {
 60             int delta = i;
 61             scope.addCloseAction(() -> acc.addAndGet(delta));
 62         }
 63         assertEquals(acc.get(), 0);
 64 
 65         if (cleaner == null) {
 66             scope.close();
 67             assertEquals(acc.get(), IntStream.range(0, N_THREADS).sum());
 68         } else {
 69             scope = null;
 70             int expected = IntStream.range(0, N_THREADS).sum();
 71             while (acc.get() != expected) {
 72                 kickGC();
 73             }
 74         }
 75     }
 76 
 77     @Test(dataProvider = "cleaners")
 78     public void testSharedSingleThread(Supplier<Cleaner> cleanerSupplier) {
 79         AtomicInteger acc = new AtomicInteger();
 80         Cleaner cleaner = cleanerSupplier.get();
 81         ResourceScope scope = cleaner != null ?
 82                 ResourceScope.newSharedScope(cleaner) :
 83                 ResourceScope.newSharedScope();
 84         for (int i = 0 ; i < N_THREADS ; i++) {
 85             int delta = i;
 86             scope.addCloseAction(() -> acc.addAndGet(delta));
 87         }
 88         assertEquals(acc.get(), 0);
 89 
 90         if (cleaner == null) {
 91             scope.close();
 92             assertEquals(acc.get(), IntStream.range(0, N_THREADS).sum());
 93         } else {
 94             scope = null;
 95             int expected = IntStream.range(0, N_THREADS).sum();
 96             while (acc.get() != expected) {
 97                 kickGC();
 98             }
 99         }
100     }
101 
102     @Test(dataProvider = "cleaners")
103     public void testSharedMultiThread(Supplier<Cleaner> cleanerSupplier) {
104         AtomicInteger acc = new AtomicInteger();
105         Cleaner cleaner = cleanerSupplier.get();
106         List<Thread> threads = new ArrayList<>();
107         ResourceScope scope = cleaner != null ?
108                 ResourceScope.newSharedScope(cleaner) :
109                 ResourceScope.newSharedScope();
110         AtomicReference<ResourceScope> scopeRef = new AtomicReference<>(scope);
111         for (int i = 0 ; i < N_THREADS ; i++) {
112             int delta = i;
113             Thread thread = new Thread(() -> {
114                 try {
115                     scopeRef.get().addCloseAction(() -> {
116                         acc.addAndGet(delta);
117                     });
118                 } catch (IllegalStateException ex) {
119                     // already closed - we need to call cleanup manually
120                     acc.addAndGet(delta);
121                 }
122             });
123             threads.add(thread);
124         }
125         assertEquals(acc.get(), 0);
126         threads.forEach(Thread::start);
127 
128         // if no cleaner, close - not all segments might have been added to the scope!
129         // if cleaner, don't unset the scope - after all, the scope is kept alive by threads
130         if (cleaner == null) {
131             while (true) {
132                 try {
133                     scope.close();
134                     break;
135                 } catch (IllegalStateException ise) {
136                     // scope is acquired (by add) - wait some more
137                 }
138             }
139         }
140 
141         threads.forEach(t -> {
142             try {
143                 t.join();
144             } catch (InterruptedException ex) {
145                 fail();
146             }
147         });
148 
149         if (cleaner == null) {
150             assertEquals(acc.get(), IntStream.range(0, N_THREADS).sum());
151         } else {
152             scope = null;
153             scopeRef.set(null);
154             int expected = IntStream.range(0, N_THREADS).sum();
155             while (acc.get() != expected) {
156                 kickGC();
157             }
158         }
159     }
160 
161     @Test
162     public void testLockSingleThread() {
163         ResourceScope scope = ResourceScope.newConfinedScope();
164         List<ResourceScope> handles = new ArrayList<>();
165         for (int i = 0 ; i < N_THREADS ; i++) {
166             ResourceScope handle = ResourceScope.newConfinedScope();
167             handle.keepAlive(scope);
168             handles.add(handle);
169         }
170 
171         while (true) {
172             try {
173                 scope.close();
174                 assertEquals(handles.size(), 0);
175                 break;
176             } catch (IllegalStateException ex) {
177                 assertTrue(handles.size() > 0);
178                 ResourceScope handle = handles.remove(0);
179                 handle.close();
180             }
181         }
182     }
183 
184     @Test
185     public void testLockSharedMultiThread() {
186         ResourceScope scope = ResourceScope.newSharedScope();
187         AtomicInteger lockCount = new AtomicInteger();
188         for (int i = 0 ; i < N_THREADS ; i++) {
189             new Thread(() -> {
190                 try (ResourceScope handle = ResourceScope.newConfinedScope()) {
191                     handle.keepAlive(scope);
192                     lockCount.incrementAndGet();
193                     waitSomeTime();
194                     lockCount.decrementAndGet();
195                     handle.close();
196                 } catch (IllegalStateException ex) {
197                     // might be already closed - do nothing
198                 }
199             }).start();
200         }
201 
202         while (true) {
203             try {
204                 scope.close();
205                 assertEquals(lockCount.get(), 0);
206                 break;
207             } catch (IllegalStateException ex) {
208                 waitSomeTime();
209             }
210         }
211     }
212 
213     @Test
214     public void testCloseEmptyConfinedScope() {
215         ResourceScope.newConfinedScope().close();
216     }
217 
218     @Test
219     public void testCloseEmptySharedScope() {
220         ResourceScope.newSharedScope().close();
221     }
222 
223     @Test
224     public void testCloseConfinedLock() {
225         ResourceScope scope = ResourceScope.newConfinedScope();
226         ResourceScope handle = ResourceScope.newConfinedScope();
227         handle.keepAlive(scope);
228         AtomicReference<Throwable> failure = new AtomicReference<>();
229         Thread t = new Thread(() -> {
230             try {
231                 handle.close();
232             } catch (Throwable ex) {
233                 failure.set(ex);
234             }
235         });
236         t.start();
237         try {
238             t.join();
239             assertNotNull(failure.get());
240             assertEquals(failure.get().getClass(), IllegalStateException.class);
241         } catch (Throwable ex) {
242             throw new AssertionError(ex);
243         }
244     }
245 
246     @Test(dataProvider = "scopes")
247     public void testScopeHandles(Supplier<ResourceScope> scopeFactory) {
248         ResourceScope scope = scopeFactory.get();
249         acquireRecursive(scope, 5);
250         if (scope != ResourceScope.globalScope()) {
251             scope.close();
252         }
253     }
254 
255     @Test(dataProvider = "scopes", expectedExceptions = IllegalArgumentException.class)
256     public void testAcquireSelf(Supplier<ResourceScope> scopeSupplier) {
257         ResourceScope scope = scopeSupplier.get();
258         scope.keepAlive(scope);
259     }
260 
261     private void acquireRecursive(ResourceScope scope, int acquireCount) {
262         try (ResourceScope handle = ResourceScope.newConfinedScope()) {
263             handle.keepAlive(scope);
264             if (acquireCount > 0) {
265                 // recursive acquire
266                 acquireRecursive(scope, acquireCount - 1);
267             }
268             if (scope != ResourceScope.globalScope()) {
269                 assertThrows(IllegalStateException.class, scope::close);
270             }
271         }
272     }
273 
274     @Test
275     public void testConfinedScopeWithImplicitDependency() {
276         ResourceScope root = ResourceScope.newConfinedScope();
277         // Create many implicit scopes which depend on 'root', and let them become unreachable.
278         for (int i = 0; i < N_THREADS; i++) {
279             ResourceScope.newConfinedScope(Cleaner.create()).keepAlive(root);
280         }
281         // Now let's keep trying to close 'root' until we succeed. This is trickier than it seems: cleanup action
282         // might be called from another thread (the Cleaner thread), so that the confined scope lock count is updated racily.
283         // If that happens, the loop below never terminates.
284         while (true) {
285             try {
286                 root.close();
287                 break; // success!
288             } catch (IllegalStateException ex) {
289                 kickGC();
290                 for (int i = 0 ; i < N_THREADS ; i++) {  // add more races from current thread
291                     try (ResourceScope scope = ResourceScope.newConfinedScope()) {
292                         scope.keepAlive(root);
293                         // dummy
294                     }
295                 }
296                 // try again
297             }
298         }
299     }
300 
301     @Test
302     public void testConfinedScopeWithSharedDependency() {
303         ResourceScope root = ResourceScope.newConfinedScope();
304         List<Thread> threads = new ArrayList<>();
305         // Create many implicit scopes which depend on 'root', and let them become unreachable.
306         for (int i = 0; i < N_THREADS; i++) {
307             ResourceScope scope = ResourceScope.newSharedScope(); // create scope inside same thread!
308             scope.keepAlive(root);
309             Thread t = new Thread(scope::close); // close from another thread!
310             threads.add(t);
311             t.start();
312         }
313         for (int i = 0 ; i < N_THREADS ; i++) { // add more races from current thread
314             try (ResourceScope scope = ResourceScope.newConfinedScope()) {
315                 scope.keepAlive(root);
316                 // dummy
317             }
318         }
319         threads.forEach(t -> {
320             try {
321                 t.join();
322             } catch (InterruptedException ex) {
323                 // ok
324             }
325         });
326         // Now let's close 'root'. This is trickier than it seems: releases of the confined scope happen in different
327         // threads, so that the confined scope lock count is updated racily. If that happens, the following close will blow up.
328         root.close();
329     }
330 
331     private void waitSomeTime() {
332         try {
333             Thread.sleep(10);
334         } catch (InterruptedException ex) {
335             // ignore
336         }
337     }
338 
339     private void kickGC() {
340         for (int i = 0 ; i < 100 ; i++) {
341             byte[] b = new byte[100];
342             System.gc();
343             Thread.onSpinWait();
344         }
345     }
346 
347     @DataProvider
348     static Object[][] cleaners() {
349         return new Object[][] {
350                 { (Supplier<Cleaner>)() -> null },
351                 { (Supplier<Cleaner>)Cleaner::create },
352                 { (Supplier<Cleaner>)CleanerFactory::cleaner }
353         };
354     }
355 
356     @DataProvider
357     static Object[][] scopes() {
358         return new Object[][] {
359                 { (Supplier<ResourceScope>)ResourceScope::newConfinedScope },
360                 { (Supplier<ResourceScope>)ResourceScope::newSharedScope },
361                 { (Supplier<ResourceScope>)ResourceScope::newImplicitScope },
362                 { (Supplier<ResourceScope>)ResourceScope::globalScope }
363         };
364     }
365 }