Repository navigation
perf(tck): lease Compose stacks from a warmed pool #1953
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1b00e23
e1422cd
ded590b
aa8b502
77be6a3
82497e9
518ce4b
5658a29
8e71397
ee0f8a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| package dev.openfeature.contrib.tools.tck.internal.steps; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.concurrent.BlockingQueue; | ||
| import java.util.concurrent.ExecutionException; | ||
| import java.util.concurrent.ExecutorService; | ||
| import java.util.concurrent.Executors; | ||
| import java.util.concurrent.Future; | ||
| import java.util.concurrent.LinkedBlockingQueue; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import java.util.function.Supplier; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * Backends warmed once and leased to one scenario at a time. | ||
| * | ||
| * <p>Off by default: with no size set, every {@link TckObjectFactory} starts one backend and keeps | ||
| * it for as long as its thread lives. The size caps how many stand at once, which a thread count | ||
| * cannot do: {@code cucumber.execution.parallel.config.fixed.parallelism} is a target, not a limit. | ||
| * Surplus threads queue on {@link #acquire()}. | ||
| */ | ||
| final class StackPool { | ||
|
|
||
| /** How many backends to warm; absent or zero keeps one backend per thread. */ | ||
| static final String SIZE_PROPERTY = "openfeature.tck.pool.size"; | ||
|
|
||
| /** Every lease is returned by {@code stop()}, so waiting this long means one was lost. */ | ||
| private static final long ACQUIRE_TIMEOUT_MINUTES = 10; | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(StackPool.class); | ||
|
|
||
| private static final BlockingQueue<TckLifecycle> idle = new LinkedBlockingQueue<>(); | ||
|
|
||
| private static final ThreadLocal<Boolean> leaseBroken = ThreadLocal.withInitial(() -> false); | ||
|
|
||
| private static volatile int size; | ||
|
|
||
| /** Backends still fit to lease; drops lower it, so waiting scenarios can fail at zero. */ | ||
| private static final AtomicInteger live = new AtomicInteger(); | ||
|
|
||
| private StackPool() { | ||
| // warmed and drained by TckLifecycle, leased by TckObjectFactory | ||
| } | ||
|
|
||
| /** Whether scenarios lease backends from this pool. */ | ||
| static boolean enabled() { | ||
| return size > 0; | ||
| } | ||
|
|
||
| /** | ||
| * Starts the requested number of backends, concurrently, or does nothing when none was asked | ||
| * for. | ||
| * | ||
| * @param start supplies one started backend, as the per-thread path does | ||
| * @throws IllegalStateException if any backend fails to start | ||
| */ | ||
| static void warm(Supplier<TckLifecycle> start) { | ||
| int requested = requestedSize(); | ||
| if (requested <= 0) { | ||
| size = 0; | ||
| return; | ||
| } | ||
|
|
||
| log.info("Warming {} backends ({}={})", requested, SIZE_PROPERTY, requested); | ||
| ExecutorService starters = Executors.newFixedThreadPool(requested); | ||
| List<Future<TckLifecycle>> futures = new ArrayList<>(); | ||
| try { | ||
| for (int i = 0; i < requested; i++) { | ||
| futures.add(starters.submit(start::get)); | ||
| } | ||
| List<TckLifecycle> warmed = new ArrayList<>(); | ||
| RuntimeException failure = null; | ||
| for (Future<TckLifecycle> future : futures) { | ||
| try { | ||
| warmed.add(future.get()); | ||
| } catch (ExecutionException e) { | ||
| RuntimeException cause = e.getCause() instanceof RuntimeException | ||
| ? (RuntimeException) e.getCause() | ||
| : new IllegalStateException(e.getCause()); | ||
| failure = failure == null ? cause : failure; | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| throw new IllegalStateException("interrupted while warming the backend pool", e); | ||
| } | ||
| } | ||
| if (failure != null) { | ||
| // failing beats letting scenarios queue for backends that will never arrive | ||
| throw new IllegalStateException( | ||
| "only " + warmed.size() + " of " + requested + " backends started", failure); | ||
| } | ||
| idle.addAll(warmed); | ||
| size = requested; | ||
| live.set(requested); | ||
| } finally { | ||
| starters.shutdownNow(); | ||
| } | ||
| } | ||
|
|
||
| /** Takes a backend for one scenario, waiting while every one of them is in use. */ | ||
| static TckLifecycle acquire() { | ||
| leaseBroken.set(false); | ||
| long deadline = System.nanoTime() + TimeUnit.MINUTES.toNanos(ACQUIRE_TIMEOUT_MINUTES); | ||
| try { | ||
| while (System.nanoTime() < deadline) { | ||
| if (live.get() <= 0) { | ||
| throw new IllegalStateException("every backend was dropped after its preparation failed (" | ||
| + SIZE_PROPERTY + "=" + size + "); there is nothing left to lease"); | ||
| } | ||
| // in steps, so a scenario does not wait out the whole deadline for a pool that emptied | ||
| TckLifecycle leased = idle.poll(1, TimeUnit.SECONDS); | ||
| if (leased != null) { | ||
| return leased; | ||
| } | ||
| } | ||
| throw new IllegalStateException("no backend became free within " + ACQUIRE_TIMEOUT_MINUTES | ||
| + " minutes, with " + live.get() + " of " + size + " in the pool (" + SIZE_PROPERTY | ||
| + "); a lease was not returned"); | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| throw new IllegalStateException("interrupted while waiting for a backend", e); | ||
| } | ||
| } | ||
|
|
||
| /** Returns a leased backend, dropping it if the scenario reported it broken. */ | ||
| static void release(TckLifecycle lifecycle) { | ||
| if (Boolean.TRUE.equals(leaseBroken.get())) { | ||
| leaseBroken.set(false); | ||
| log.warn( | ||
| "Dropping a backend whose preparation failed; {} of {} left in the pool", | ||
| live.decrementAndGet(), | ||
| size); | ||
| return; | ||
| } | ||
| idle.add(lifecycle); | ||
| } | ||
|
|
||
| /** Marks the backend this thread holds as unfit to pass on; called where the breakage shows. */ | ||
| static void markLeaseBroken() { | ||
| if (enabled()) { | ||
| leaseBroken.set(true); | ||
| } | ||
| } | ||
|
|
||
| /** Forgets every lease; the backends themselves are stopped by {@code endSuite()}. */ | ||
| static void drain() { | ||
| idle.clear(); | ||
| size = 0; | ||
| live.set(0); | ||
| } | ||
|
|
||
| private static int requestedSize() { | ||
| String raw = System.getProperty(SIZE_PROPERTY); | ||
| if (raw == null || raw.trim().isEmpty()) { | ||
| return 0; | ||
| } | ||
| try { | ||
| int parsed = Integer.parseInt(raw.trim()); | ||
| if (parsed < 0) { | ||
| throw new IllegalArgumentException( | ||
| SIZE_PROPERTY + " is " + raw + "; a pool cannot hold fewer than no backends."); | ||
| } | ||
| return parsed; | ||
| } catch (NumberFormatException e) { | ||
| throw new IllegalArgumentException( | ||
| SIZE_PROPERTY + " is '" + raw + "', which is not a number of backends.", e); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,9 @@ public final class TckObjectFactory implements ObjectFactory { | |
|
|
||
| private TckLifecycle lifecycle; | ||
|
|
||
| /** The pooled backend this scenario holds, or null when backends are per-thread. */ | ||
| private TckLifecycle leased; | ||
|
|
||
| @Override | ||
| public boolean addClass(Class<?> glueClass) { | ||
| if (isInstantiable(glueClass) && glue.add(glueClass)) { | ||
|
|
@@ -60,7 +63,11 @@ private static boolean isInstantiable(Class<?> candidate) { | |
| @Override | ||
| public void start() { | ||
| pico = new PicoBuilder().withCaching().build(); | ||
| if (lifecycle == null) { | ||
| if (StackPool.enabled()) { | ||
| // the pool's size, not this thread's existence, decides how many stand at once | ||
| leased = StackPool.acquire(); | ||
| lifecycle = leased; | ||
| } else if (lifecycle == null) { | ||
| // empty when a launcher selects only the extension glue and never reaches @BeforeAll | ||
| lifecycle = TckLifecycle.startForExecutingSuite().orElse(null); | ||
| } | ||
|
|
@@ -76,10 +83,19 @@ public void start() { | |
|
|
||
| @Override | ||
| public void stop() { | ||
| if (pico != null) { | ||
| pico.stop(); | ||
| pico.dispose(); | ||
| pico = null; | ||
| try { | ||
| if (pico != null) { | ||
| pico.stop(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| pico.dispose(); | ||
| pico = null; | ||
| } | ||
| } finally { | ||
| if (leased != null) { | ||
| // after the container is disposed, so nothing can still be using the backend | ||
| StackPool.release(leased); | ||
| leased = null; | ||
| lifecycle = null; | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.