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.concurrent.atomic.AtomicInteger;
 43 import java.util.concurrent.atomic.AtomicReference;
 44 import java.util.function.Supplier;
 45 import java.util.stream.IntStream;
 46 
 47 public class TestResourceScope {
 48 
 49     final static int N_THREADS = 100;
 50 
 51     @Test(dataProvider = "cleaners")
 52     public void testConfined(Supplier<Cleaner> cleanerSupplier) {
 53         AtomicInteger acc = new AtomicInteger();
 54         Cleaner cleaner = cleanerSupplier.get();
 55         ResourceScope scope = cleaner != null ?
 56                 ResourceScope.newConfinedScope(cleaner) :
 57                 ResourceScope.newConfinedScope();
 58         for (int i = 0 ; i < N_THREADS ; i++) {
 59             int delta = i;
 60             scope.addCloseAction(() -> acc.addAndGet(delta));
 61         }
 62         assertEquals(acc.get(), 0);
 63 
 64         if (cleaner == null) {
 65             scope.close();
 66             assertEquals(acc.get(), IntStream.range(0, N_THREADS).sum());
 67         } else {
 68             scope = null;
 69             int expected = IntStream.range(0, N_THREADS).sum();
 70             while (acc.get() != expected) {
 71                 kickGC();
 72             }
 73         }
 74     }
 75 
 76     @Test(dataProvider = "cleaners")
 77     public void testSharedSingleThread(Supplier<Cleaner> cleanerSupplier) {
 78         AtomicInteger acc = new AtomicInteger();
 79         Cleaner cleaner = cleanerSupplier.get();
 80         ResourceScope scope = cleaner != null ?
 81                 ResourceScope.newSharedScope(cleaner) :
 82                 ResourceScope.newSharedScope();
 83         for (int i = 0 ; i < N_THREADS ; i++) {
 84             int delta = i;
 85             scope.addCloseAction(() -> acc.addAndGet(delta));
 86         }
 87         assertEquals(acc.get(), 0);
 88 
 89         if (cleaner == null) {
 90             scope.close();
 91             assertEquals(acc.get(), IntStream.range(0, N_THREADS).sum());
 92         } else {
 93             scope = null;
 94             int expected = IntStream.range(0, N_THREADS).sum();
 95             while (acc.get() != expected) {
 96                 kickGC();
 97             }
 98         }
 99     }
100 
101     @Test(dataProvider = "cleaners")
102     public void testSharedMultiThread(Supplier<Cleaner> cleanerSupplier) {
103         AtomicInteger acc = new AtomicInteger();
104         Cleaner cleaner = cleanerSupplier.get();
105         List<Thread> threads = new ArrayList<>();
106         ResourceScope scope = cleaner != null ?
107                 ResourceScope.newSharedScope(cleaner) :
108                 ResourceScope.newSharedScope();
109         AtomicReference<ResourceScope> scopeRef = new AtomicReference<>(scope);
110         for (int i = 0 ; i < N_THREADS ; i++) {
111             int delta = i;
112             Thread thread = new Thread(() -> {
113                 try {
114                     scopeRef.get().addCloseAction(() -> {
115                         acc.addAndGet(delta);
116                     });
117                 } catch (IllegalStateException ex) {
118                     // already closed - we need to call cleanup manually
119                     acc.addAndGet(delta);
120                 }
121             });
122             threads.add(thread);
123         }
124         assertEquals(acc.get(), 0);
125         threads.forEach(Thread::start);
126 
127         // if no cleaner, close - not all segments might have been added to the scope!
128         // if cleaner, don't unset the scope - after all, the scope is kept alive by threads
129         if (cleaner == null) {
130             while (true) {
131                 try {
132                     scope.close();
133                     break;
134                 } catch (IllegalStateException ise) {
135                     // scope is acquired (by add) - wait some more
136                 }
137             }
138         }
139 
140         threads.forEach(t -> {
141             try {
142                 t.join();
143             } catch (InterruptedException ex) {
144                 fail();
145             }
146         });
147 
148         if (cleaner == null) {
149             assertEquals(acc.get(), IntStream.range(0, N_THREADS).sum());
150         } else {
151             scope = null;
152             scopeRef.set(null);
153             int expected = IntStream.range(0, N_THREADS).sum();
154             while (acc.get() != expected) {
155                 kickGC();
156             }
157         }
158     }
159 
160     @Test(dataProvider = "cleaners")
161     public void testLockSingleThread(Supplier<Cleaner> cleanerSupplier) {
162         Cleaner cleaner = cleanerSupplier.get();
163         ResourceScope scope = cleaner != null ?
164                 ResourceScope.newConfinedScope(cleaner) :
165                 ResourceScope.newConfinedScope();
166         List<ResourceScope.Handle> handles = new ArrayList<>();
167         for (int i = 0 ; i < N_THREADS ; i++) {
168             handles.add(scope.acquire());
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 handle = handles.remove(0);
179                 scope.release(handle);
180                 scope.release(handle); // make sure it's idempotent
181                 scope.release(handle); // make sure it's idempotent
182             }
183         }
184     }
185 
186     @Test(dataProvider = "cleaners")
187     public void testLockSharedMultiThread(Supplier<Cleaner> cleanerSupplier) {
188         Cleaner cleaner = cleanerSupplier.get();
189         ResourceScope scope = cleaner != null ?
190                 ResourceScope.newSharedScope(cleaner) :
191                 ResourceScope.newSharedScope();
192         AtomicInteger lockCount = new AtomicInteger();
193         for (int i = 0 ; i < N_THREADS ; i++) {
194             new Thread(() -> {
195                 try {
196                     ResourceScope.Handle handle = scope.acquire(); // this can throw if segment has been closed
197                     lockCount.incrementAndGet();
198                     waitSomeTime();
199                     lockCount.decrementAndGet();
200                     scope.release(handle); // cannot throw (acquired segments cannot be closed)
201                     scope.release(handle); // cannot throw (idempotent)
202                     scope.release(handle); // cannot throw (idempotent)
203                 } catch (IllegalStateException ex) {
204                     // might be already closed - do nothing
205                 }
206             }).start();
207         }
208 
209         while (true) {
210             try {
211                 scope.close();
212                 assertEquals(lockCount.get(), 0);
213                 break;
214             } catch (IllegalStateException ex) {
215                 waitSomeTime();
216             }
217         }
218     }
219 
220     @Test
221     public void testCloseEmptyConfinedScope() {
222         ResourceScope.newConfinedScope().close();
223     }
224 
225     @Test
226     public void testCloseEmptySharedScope() {
227         ResourceScope.newSharedScope().close();
228     }
229 
230     @Test
231     public void testCloseConfinedLock() {
232         ResourceScope scope = ResourceScope.newConfinedScope();
233         ResourceScope.Handle handle = scope.acquire();
234         AtomicReference<Throwable> failure = new AtomicReference<>();
235         Thread t = new Thread(() -> {
236             try {
237                 scope.release(handle);
238                 scope.release(handle); // make sure it's idempotent
239                 scope.release(handle); // make sure it's idempotent
240             } catch (Throwable ex) {
241                 failure.set(ex);
242             }
243         });
244         t.start();
245         try {
246             t.join();
247             assertNotNull(failure.get());
248             assertEquals(failure.get().getClass(), IllegalStateException.class);
249         } catch (Throwable ex) {
250             throw new AssertionError(ex);
251         }
252     }
253 
254     @Test(dataProvider = "scopes")
255     public void testScopeHandles(Supplier<ResourceScope> scopeFactory) {
256         ResourceScope scope = scopeFactory.get();
257         acquireRecursive(scope, 5);
258         if (!scope.isImplicit()) {
259             scope.close();
260         }
261     }
262 
263     private void acquireRecursive(ResourceScope scope, int acquireCount) {
264         ResourceScope.Handle handle = scope.acquire();
265         assertEquals(handle.scope(), scope);
266         if (acquireCount > 0) {
267             // recursive acquire
268             acquireRecursive(scope, acquireCount - 1);
269         }
270         if (!scope.isImplicit()) {
271             assertThrows(IllegalStateException.class, scope::close);
272         }
273         scope.release(handle);
274         scope.release(handle); // make sure it's idempotent
275         scope.release(handle); // make sure it's idempotent
276     }
277 
278     private void waitSomeTime() {
279         try {
280             Thread.sleep(10);
281         } catch (InterruptedException ex) {
282             // ignore
283         }
284     }
285 
286     private void kickGC() {
287         for (int i = 0 ; i < 100 ; i++) {
288             byte[] b = new byte[100];
289             System.gc();
290             Thread.onSpinWait();
291         }
292     }
293 
294     @DataProvider
295     static Object[][] cleaners() {
296         return new Object[][] {
297                 { (Supplier<Cleaner>)() -> null },
298                 { (Supplier<Cleaner>)Cleaner::create },
299                 { (Supplier<Cleaner>)CleanerFactory::cleaner }
300         };
301     }
302 
303     @DataProvider
304     static Object[][] scopes() {
305         return new Object[][] {
306                 { (Supplier<ResourceScope>)ResourceScope::newConfinedScope },
307                 { (Supplier<ResourceScope>)ResourceScope::newSharedScope },
308                 { (Supplier<ResourceScope>)ResourceScope::newImplicitScope },
309                 { (Supplier<ResourceScope>)ResourceScope::globalScope }
310         };
311     }
312 }