/* * Copyright (c) 2021, 2024, 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 * @modules java.base/jdk.internal.foreign * @run testng/othervm TestMemorySession */ import java.lang.foreign.Arena; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import java.util.stream.IntStream; import jdk.internal.foreign.MemorySessionImpl; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import static org.testng.Assert.*; public class TestMemorySession { final static int N_THREADS = 100; @Test public void testConfined() { AtomicInteger acc = new AtomicInteger(); Arena arena = Arena.ofConfined(); for (int i = 0 ; i < N_THREADS ; i++) { int delta = i; addCloseAction(arena, () -> acc.addAndGet(delta)); } assertEquals(acc.get(), 0); arena.close(); assertEquals(acc.get(), IntStream.range(0, N_THREADS).sum()); } @Test(dataProvider = "sharedSessions") public void testSharedSingleThread(ArenaSupplier arenaSupplier) { AtomicInteger acc = new AtomicInteger(); Arena session = arenaSupplier.get(); for (int i = 0 ; i < N_THREADS ; i++) { int delta = i; addCloseAction(session, () -> acc.addAndGet(delta)); } assertEquals(acc.get(), 0); if (!TestMemorySession.ArenaSupplier.isImplicit(session)) { TestMemorySession.ArenaSupplier.close(session); assertEquals(acc.get(), IntStream.range(0, N_THREADS).sum()); } else { session = null; int expected = IntStream.range(0, N_THREADS).sum(); while (acc.get() != expected) { kickGC(); } } } @Test(dataProvider = "sharedSessions") public void testSharedMultiThread(ArenaSupplier arenaSupplier) { AtomicInteger acc = new AtomicInteger(); List threads = new ArrayList<>(); Arena session = arenaSupplier.get(); AtomicReference sessionRef = new AtomicReference<>(session); for (int i = 0 ; i < N_THREADS ; i++) { int delta = i; Thread thread = new Thread(() -> { try { addCloseAction(sessionRef.get(), () -> { acc.addAndGet(delta); }); } catch (IllegalStateException ex) { // already closed - we need to call cleanup manually acc.addAndGet(delta); } }); threads.add(thread); } assertEquals(acc.get(), 0); threads.forEach(Thread::start); // if no cleaner, close - not all segments might have been added to the session! // if cleaner, don't unset the session - after all, the session is kept alive by threads if (!TestMemorySession.ArenaSupplier.isImplicit(session)) { while (true) { try { TestMemorySession.ArenaSupplier.close(session); break; } catch (IllegalStateException ise) { // session is acquired (by add) - wait some more } } } threads.forEach(t -> { try { t.join(); } catch (InterruptedException ex) { fail(); } }); if (!TestMemorySession.ArenaSupplier.isImplicit(session)) { assertEquals(acc.get(), IntStream.range(0, N_THREADS).sum()); } else { session = null; sessionRef.set(null); int expected = IntStream.range(0, N_THREADS).sum(); while (acc.get() != expected) { kickGC(); } } } @Test public void testLockSingleThread() { Arena arena = Arena.ofConfined(); List handles = new ArrayList<>(); for (int i = 0 ; i < N_THREADS ; i++) { Arena handle = Arena.ofConfined(); keepAlive(handle, arena); handles.add(handle); } while (true) { try { arena.close(); assertEquals(handles.size(), 0); break; } catch (IllegalStateException ex) { assertTrue(handles.size() > 0); Arena handle = handles.remove(0); handle.close(); } } } @Test public void testLockSharedMultiThread() { Arena arena = Arena.ofShared(); AtomicInteger lockCount = new AtomicInteger(); for (int i = 0 ; i < N_THREADS ; i++) { new Thread(() -> { try (Arena handle = Arena.ofConfined()) { keepAlive(handle, arena); lockCount.incrementAndGet(); waitSomeTime(); lockCount.decrementAndGet(); } catch (IllegalStateException ex) { // might be already closed - do nothing } }).start(); } while (true) { try { arena.close(); assertEquals(lockCount.get(), 0); break; } catch (IllegalStateException ex) { waitSomeTime(); } } } @Test public void testCloseEmptyConfinedSession() { Arena.ofConfined().close(); } @Test public void testCloseEmptySharedSession() { Arena.ofShared().close(); } @Test public void testCloseConfinedLock() { Arena arena = Arena.ofConfined(); Arena handle = Arena.ofConfined(); keepAlive(handle, arena); AtomicReference failure = new AtomicReference<>(); Thread t = new Thread(() -> { try { handle.close(); } catch (Throwable ex) { failure.set(ex); } }); t.start(); try { t.join(); assertNotNull(failure.get()); assertEquals(failure.get().getClass(), WrongThreadException.class); } catch (Throwable ex) { throw new AssertionError(ex); } } @Test(dataProvider = "allSessions") public void testSessionAcquires(ArenaSupplier ArenaSupplier) { Arena session = ArenaSupplier.get(); acquireRecursive(session, 5); if (!TestMemorySession.ArenaSupplier.isImplicit(session)) TestMemorySession.ArenaSupplier.close(session); } private void acquireRecursive(Arena session, int acquireCount) { try (Arena arena = Arena.ofConfined()) { keepAlive(arena, session); if (acquireCount > 0) { // recursive acquire acquireRecursive(session, acquireCount - 1); } if (!ArenaSupplier.isImplicit(session)) { assertThrows(IllegalStateException.class, () -> ArenaSupplier.close(session)); } } } @Test public void testConfinedSessionWithImplicitDependency() { Arena root = Arena.ofConfined(); // Create many implicit sessions which depend on 'root', and let them become unreachable. for (int i = 0; i < N_THREADS; i++) { keepAlive(Arena.ofAuto(), root); } // Now let's keep trying to close 'root' until we succeed. This is trickier than it seems: cleanup action // might be called from another thread (the Cleaner thread), so that the confined session lock count is updated racily. // If that happens, the loop below never terminates. while (true) { try { root.close(); break; // success! } catch (IllegalStateException ex) { kickGC(); for (int i = 0 ; i < N_THREADS ; i++) { // add more races from current thread try (Arena arena = Arena.ofConfined()) { keepAlive(arena, root); // dummy } } // try again } } } @Test public void testConfinedSessionWithSharedDependency() { Arena root = Arena.ofConfined(); List threads = new ArrayList<>(); // Create many implicit sessions which depend on 'root', and let them become unreachable. for (int i = 0; i < N_THREADS; i++) { Arena arena = Arena.ofShared(); // create session inside same thread! keepAlive(arena, root); Thread t = new Thread(arena::close); // close from another thread! threads.add(t); t.start(); } for (int i = 0 ; i < N_THREADS ; i++) { // add more races from current thread try (Arena arena = Arena.ofConfined()) { keepAlive(arena, root); // dummy } } threads.forEach(t -> { try { t.join(); } catch (InterruptedException ex) { // ok } }); // Now let's close 'root'. This is trickier than it seems: releases of the confined session happen in different // threads, so that the confined session lock count is updated racily. If that happens, the following close will blow up. root.close(); } @Test(dataProvider = "nonCloseableSessions") public void testNonCloseableSessions(ArenaSupplier arenaSupplier) { var arena = arenaSupplier.get(); var sessionImpl = ((MemorySessionImpl) arena.scope()); assertFalse(sessionImpl.isCloseable()); assertThrows(UnsupportedOperationException.class, () -> sessionImpl.close()); } @Test(dataProvider = "allSessionsAndGlobal") public void testIsCloseableBy(ArenaSupplier arenaSupplier) { var arena = arenaSupplier.get(); var sessionImpl = ((MemorySessionImpl) arena.scope()); assertEquals(sessionImpl.isCloseableBy(Thread.currentThread()), sessionImpl.isCloseable()); Thread otherThread = new Thread(); boolean isCloseableByOther = sessionImpl.isCloseable() && !"ConfinedSession".equals(sessionImpl.getClass().getSimpleName()); assertEquals(sessionImpl.isCloseableBy(otherThread), isCloseableByOther); } /** * Test that a thread failing to acquire a scope will not observe it as alive afterwards. */ @Test public void testAcquireCloseRace() throws InterruptedException { int iteration = 1000; AtomicInteger lock = new AtomicInteger(); boolean[] result = new boolean[1]; MemorySessionImpl[] scopes = new MemorySessionImpl[iteration]; for (int i = 0; i < iteration; i++) { scopes[i] = MemorySessionImpl.toMemorySession(Arena.ofShared()); } // These two threads proceed the scopes array in a lock-step manner, the first thread wait // for the second thread on the lock variable, while the second thread wait for the first // thread on the closing of the current scope // This thread tries to close the scopes Thread t1 = new Thread(() -> { for (int i = 0; i < iteration;) { MemorySessionImpl scope = scopes[i]; while (true) { try { scope.close(); // Continue to the next iteration after a successful close break; } catch (IllegalStateException e) { // Wait for the release and try again } } // Wait for the other thread to complete its iteration int prev = i; while (prev == i) { i = lock.get(); Thread.onSpinWait(); } } }); // This thread tries to acquire the scopes, then check if it is alive after an acquire failure Thread t2 = new Thread(() -> { for (int i = 0; i < iteration;) { MemorySessionImpl scope = scopes[i]; while (true) { try { scope.acquire0(); } catch (IllegalStateException e) { // The scope has been closed, proceed to the next iteration if (scope.isAlive()) { result[0] = true; } break; } // Release and try again scope.release0(); } // Proceed to the next iteration i = lock.getAndAdd(1) + 1; } }); t1.start(); t2.start(); t1.join(); t2.join(); assertFalse(result[0]); } private void waitSomeTime() { try { Thread.sleep(10); } catch (InterruptedException ex) { // ignore } } private void kickGC() { for (int i = 0 ; i < 100 ; i++) { byte[] b = new byte[100]; System.gc(); Thread.onSpinWait(); } } @DataProvider static Object[][] drops() { return new Object[][] { { (Supplier) Arena::ofConfined}, { (Supplier) Arena::ofShared}, }; } private void keepAlive(Arena child, Arena parent) { MemorySessionImpl parentImpl = MemorySessionImpl.toMemorySession(parent); parentImpl.acquire0(); addCloseAction(child, parentImpl::release0); } private void addCloseAction(Arena session, Runnable action) { MemorySessionImpl sessionImpl = MemorySessionImpl.toMemorySession(session); sessionImpl.addCloseAction(action); } interface ArenaSupplier extends Supplier { static void close(Arena arena) { MemorySessionImpl.toMemorySession(arena).close(); } static boolean isImplicit(Arena arena) { return !MemorySessionImpl.toMemorySession(arena).isCloseable(); } static ArenaSupplier ofAuto() { return Arena::ofAuto; } static ArenaSupplier ofGlobal() { return Arena::global; } static ArenaSupplier ofArena(Supplier arenaSupplier) { return arenaSupplier::get; } } @DataProvider(name = "sharedSessions") static Object[][] sharedSessions() { return new Object[][] { { ArenaSupplier.ofArena(Arena::ofShared) }, { ArenaSupplier.ofAuto() }, }; } @DataProvider(name = "allSessions") static Object[][] allSessions() { return new Object[][] { { ArenaSupplier.ofArena(Arena::ofConfined) }, { ArenaSupplier.ofArena(Arena::ofShared) }, { ArenaSupplier.ofAuto() }, }; } @DataProvider(name = "nonCloseableSessions") static Object[][] nonCloseableSessions() { return new Object[][] { { ArenaSupplier.ofGlobal() }, { ArenaSupplier.ofAuto() } }; } @DataProvider(name = "allSessionsAndGlobal") static Object[][] allSessionsAndGlobal() { return new Object[][] { { ArenaSupplier.ofArena(Arena::ofConfined) }, { ArenaSupplier.ofArena(Arena::ofShared) }, { ArenaSupplier.ofAuto() }, { ArenaSupplier.ofGlobal() }, }; } }