# HG changeset patch # User cin # Date 2026-04-20 19:18:48 # Node ID 07d0d84bc0a28bcde22802e1e9a1a412012c4456 # Parent 3939ecb6e9a430bf1855c40573dfb0bcbc7d2866 common: extract replayable queue diff --git a/common/src/main/java/org/implab/gradle/common/core/lang/ReplayableQueue.java b/common/src/main/java/org/implab/gradle/common/core/lang/ReplayableQueue.java new file mode 100644 --- /dev/null +++ b/common/src/main/java/org/implab/gradle/common/core/lang/ReplayableQueue.java @@ -0,0 +1,89 @@ +package org.implab.gradle.common.core.lang; + +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Replayable append-only notification queue. + * + *

+ * The queue stores values in insertion order and supports replaying all values + * that were already added when a consumer is registered. The same consumer is + * also retained and invoked for every value added later. + * + *

+ * Values are committed before dispatch. If a consumer throws while a value is + * being added, the value remains recorded in the queue and will be visible to + * later consumers through {@link #forEach(Consumer)} and {@link #values()}. + * + *

+ * Reentrant replay is intentionally not supported. Consumers must not call + * {@link #add(Object)} or {@link #forEach(Consumer)} on the same queue while + * they are being invoked by that queue. Such nested calls fail fast with + * {@link IllegalStateException}. This keeps ordering predictable and avoids + * recursive event-loop semantics. + * + * @param value type + */ +@NonNullByDefault +public class ReplayableQueue { + private final List> consumers = new LinkedList<>(); + private final List values = new LinkedList<>(); + private boolean replaying = false; + + /** + * Adds a value and dispatches it to all registered consumers. + * + *

+ * The value is recorded before consumers are invoked. If dispatch fails, the + * value still belongs to the queue. + * + * @param value value to add + */ + public void add(T value) { + safeInvoke(value, v -> { + values.add(v); + consumers.forEach(consumer -> consumer.accept(v)); + }); + } + + /** + * Returns an immutable snapshot of values recorded so far. + * + * @return current values in insertion order + */ + public List values() { + return List.copyOf(values); + } + + /** + * Replays all recorded values to the consumer and registers it for future + * values. + * + *

+ * The consumer is registered only after replaying existing values succeeds. + * + * @param consumer consumer to replay and retain + */ + public void forEach(Consumer consumer) { + safeInvoke(values, v -> { + v.forEach(consumer); + consumers.add(consumer); + }); + } + + private void safeInvoke(X value, Consumer consumer) { + if (replaying) + throw new IllegalStateException("Reentrant replay is not supported: replay is in progress"); + try { + replaying = true; + consumer.accept(value); + } finally { + replaying = false; + } + } + +} diff --git a/common/src/test/java/org/implab/gradle/common/core/lang/ReplayableQueueTest.java b/common/src/test/java/org/implab/gradle/common/core/lang/ReplayableQueueTest.java new file mode 100644 --- /dev/null +++ b/common/src/test/java/org/implab/gradle/common/core/lang/ReplayableQueueTest.java @@ -0,0 +1,59 @@ +package org.implab.gradle.common.core.lang; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +class ReplayableQueueTest { + @Test + void replaysExistingValuesAndReceivesFutureValues() { + var queue = new ReplayableQueue(); + var seen = new ArrayList(); + + queue.add("one"); + queue.add("two"); + queue.forEach(seen::add); + queue.add("three"); + + assertEquals(List.of("one", "two", "three"), seen); + assertEquals(List.of("one", "two", "three"), queue.values()); + } + + @Test + void commitsValueBeforeDispatchingConsumers() { + var queue = new ReplayableQueue(); + + queue.forEach(value -> { + throw new IllegalStateException("boom"); + }); + + assertThrows(IllegalStateException.class, () -> queue.add("one")); + assertEquals(List.of("one"), queue.values()); + } + + @Test + void rejectsReentrantAdd() { + var queue = new ReplayableQueue(); + + queue.forEach(value -> queue.add("nested")); + + var ex = assertThrows(IllegalStateException.class, () -> queue.add("one")); + assertTrue(ex.getMessage().contains("Reentrant replay is not supported")); + } + + @Test + void rejectsReentrantForEach() { + var queue = new ReplayableQueue(); + queue.add("one"); + + var ex = assertThrows(IllegalStateException.class, () -> queue.forEach(value -> queue.forEach(nested -> { + }))); + + assertTrue(ex.getMessage().contains("Reentrant replay is not supported")); + } +} diff --git a/variants/src/main/java/org/implab/gradle/internal/ReplayableQueue.java b/variants/src/main/java/org/implab/gradle/internal/ReplayableQueue.java deleted file mode 100644 --- a/variants/src/main/java/org/implab/gradle/internal/ReplayableQueue.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.implab.gradle.internal; - -import java.util.LinkedList; -import java.util.List; -import java.util.function.Consumer; - -import org.eclipse.jdt.annotation.NonNullByDefault; - -@NonNullByDefault -public class ReplayableQueue { - private final List> consumers = new LinkedList<>(); - private final List values = new LinkedList<>(); - - public void add(T value) { - consumers.forEach(consumer -> consumer.accept(value)); - values.add(value); - } - - List values() { - return List.copyOf(values); - } - - public void forEach(Consumer consumer) { - values.forEach(consumer); - consumers.add(consumer); - } -} \ No newline at end of file diff --git a/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/ArtifactAssemblyRegistry.java b/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/ArtifactAssemblyRegistry.java --- a/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/ArtifactAssemblyRegistry.java +++ b/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/ArtifactAssemblyRegistry.java @@ -13,7 +13,7 @@ import org.gradle.api.file.FileSystemLoc import org.gradle.api.provider.Provider; import org.gradle.api.tasks.TaskProvider; import org.implab.gradle.common.core.lang.Deferred; -import org.implab.gradle.internal.ReplayableQueue; +import org.implab.gradle.common.core.lang.ReplayableQueue; import org.implab.gradle.variants.artifacts.ArtifactAssemblies; import org.implab.gradle.variants.artifacts.ArtifactAssembly; import org.implab.gradle.variants.artifacts.ArtifactSlot; diff --git a/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/OutgoingRegistry.java b/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/OutgoingRegistry.java --- a/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/OutgoingRegistry.java +++ b/variants/src/main/java/org/implab/gradle/variants/artifacts/internal/OutgoingRegistry.java @@ -15,14 +15,16 @@ import org.gradle.api.artifacts.Configur import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.Property; import org.implab.gradle.internal.IdentityContainerFactory; -import org.implab.gradle.internal.ReplayableQueue; +import org.implab.gradle.common.core.lang.ReplayableQueue; import org.implab.gradle.variants.artifacts.OutgoingVariant; import org.implab.gradle.variants.artifacts.Slot; import org.implab.gradle.variants.core.Variant; /** - * Реестр исходящих вариантов. Связывает исходящие конфигурации с вариантами - * сборки. Связь устанавливается 1:1. + * Registry of variant-level outgoing models. + * + *

Each declared outgoing model owns one lazy Gradle consumable configuration + * and one live slot identity container. */ @NonNullByDefault public class OutgoingRegistry { @@ -66,9 +68,9 @@ public class OutgoingRegistry { } /** - * Replayable hook which is applied when an outgoing variant is defined + * Registers a replayable hook for outgoing variant declarations. * - * @param action + * @param action outgoing variant action */ public void configureEach(Consumer action) { outgoingVariants.forEach(action); diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetConfigurationRegistry.java b/variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetConfigurationRegistry.java --- a/variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetConfigurationRegistry.java +++ b/variants/src/main/java/org/implab/gradle/variants/sources/internal/SourceSetConfigurationRegistry.java @@ -2,10 +2,8 @@ package org.implab.gradle.variants.sourc import java.text.MessageFormat; import java.util.LinkedHashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -14,6 +12,7 @@ import org.gradle.api.Action; import org.gradle.api.Named; import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logging; +import org.implab.gradle.common.core.lang.ReplayableQueue; import org.implab.gradle.variants.core.Layer; import org.implab.gradle.variants.core.Variant; import org.implab.gradle.variants.sources.CompileUnit; @@ -57,7 +56,7 @@ public class SourceSetConfigurationRegis sourcesByUnit.computeIfAbsent(unit, key -> new ReplayableQueue<>()), action, MessageFormat.format( - "Source set for [variant={0}, layer={1}] already materialed", + "Source set for [variant={0}, layer={1}] already materialized", unit.variant().getName(), unit.layer().getName())); } @@ -92,23 +91,4 @@ public class SourceSetConfigurationRegis sourcesByLayer.computeIfAbsent(unit.layer(), key -> new ReplayableQueue<>()).add(sourceSet); sourcesByUnit.computeIfAbsent(unit, key -> new ReplayableQueue<>()).add(sourceSet); } - - class ReplayableQueue { - private final List> consumers = new LinkedList<>(); - private final List values = new LinkedList<>(); - - public void add(T value) { - consumers.forEach(consumer -> consumer.accept(value)); - values.add(value); - } - - List values() { - return List.copyOf(values); - } - - public void forEach(Consumer consumer) { - values.forEach(consumer); - consumers.add(consumer); - } - } }