##// END OF EJS Templates
common: extract replayable queue
cin -
r53:07d0d84bc0a2 default
parent child
Show More
@@ -0,0 +1,89
1 package org.implab.gradle.common.core.lang;
2
3 import java.util.LinkedList;
4 import java.util.List;
5 import java.util.function.Consumer;
6
7 import org.eclipse.jdt.annotation.NonNullByDefault;
8
9 /**
10 * Replayable append-only notification queue.
11 *
12 * <p>
13 * The queue stores values in insertion order and supports replaying all values
14 * that were already added when a consumer is registered. The same consumer is
15 * also retained and invoked for every value added later.
16 *
17 * <p>
18 * Values are committed before dispatch. If a consumer throws while a value is
19 * being added, the value remains recorded in the queue and will be visible to
20 * later consumers through {@link #forEach(Consumer)} and {@link #values()}.
21 *
22 * <p>
23 * Reentrant replay is intentionally not supported. Consumers must not call
24 * {@link #add(Object)} or {@link #forEach(Consumer)} on the same queue while
25 * they are being invoked by that queue. Such nested calls fail fast with
26 * {@link IllegalStateException}. This keeps ordering predictable and avoids
27 * recursive event-loop semantics.
28 *
29 * @param <T> value type
30 */
31 @NonNullByDefault
32 public class ReplayableQueue<T> {
33 private final List<Consumer<? super T>> consumers = new LinkedList<>();
34 private final List<T> values = new LinkedList<>();
35 private boolean replaying = false;
36
37 /**
38 * Adds a value and dispatches it to all registered consumers.
39 *
40 * <p>
41 * The value is recorded before consumers are invoked. If dispatch fails, the
42 * value still belongs to the queue.
43 *
44 * @param value value to add
45 */
46 public void add(T value) {
47 safeInvoke(value, v -> {
48 values.add(v);
49 consumers.forEach(consumer -> consumer.accept(v));
50 });
51 }
52
53 /**
54 * Returns an immutable snapshot of values recorded so far.
55 *
56 * @return current values in insertion order
57 */
58 public List<T> values() {
59 return List.copyOf(values);
60 }
61
62 /**
63 * Replays all recorded values to the consumer and registers it for future
64 * values.
65 *
66 * <p>
67 * The consumer is registered only after replaying existing values succeeds.
68 *
69 * @param consumer consumer to replay and retain
70 */
71 public void forEach(Consumer<? super T> consumer) {
72 safeInvoke(values, v -> {
73 v.forEach(consumer);
74 consumers.add(consumer);
75 });
76 }
77
78 private <X> void safeInvoke(X value, Consumer<? super X> consumer) {
79 if (replaying)
80 throw new IllegalStateException("Reentrant replay is not supported: replay is in progress");
81 try {
82 replaying = true;
83 consumer.accept(value);
84 } finally {
85 replaying = false;
86 }
87 }
88
89 }
@@ -0,0 +1,59
1 package org.implab.gradle.common.core.lang;
2
3 import static org.junit.jupiter.api.Assertions.assertEquals;
4 import static org.junit.jupiter.api.Assertions.assertThrows;
5 import static org.junit.jupiter.api.Assertions.assertTrue;
6
7 import java.util.ArrayList;
8 import java.util.List;
9
10 import org.junit.jupiter.api.Test;
11
12 class ReplayableQueueTest {
13 @Test
14 void replaysExistingValuesAndReceivesFutureValues() {
15 var queue = new ReplayableQueue<String>();
16 var seen = new ArrayList<String>();
17
18 queue.add("one");
19 queue.add("two");
20 queue.forEach(seen::add);
21 queue.add("three");
22
23 assertEquals(List.of("one", "two", "three"), seen);
24 assertEquals(List.of("one", "two", "three"), queue.values());
25 }
26
27 @Test
28 void commitsValueBeforeDispatchingConsumers() {
29 var queue = new ReplayableQueue<String>();
30
31 queue.forEach(value -> {
32 throw new IllegalStateException("boom");
33 });
34
35 assertThrows(IllegalStateException.class, () -> queue.add("one"));
36 assertEquals(List.of("one"), queue.values());
37 }
38
39 @Test
40 void rejectsReentrantAdd() {
41 var queue = new ReplayableQueue<String>();
42
43 queue.forEach(value -> queue.add("nested"));
44
45 var ex = assertThrows(IllegalStateException.class, () -> queue.add("one"));
46 assertTrue(ex.getMessage().contains("Reentrant replay is not supported"));
47 }
48
49 @Test
50 void rejectsReentrantForEach() {
51 var queue = new ReplayableQueue<String>();
52 queue.add("one");
53
54 var ex = assertThrows(IllegalStateException.class, () -> queue.forEach(value -> queue.forEach(nested -> {
55 })));
56
57 assertTrue(ex.getMessage().contains("Reentrant replay is not supported"));
58 }
59 }
@@ -13,7 +13,7 import org.gradle.api.file.FileSystemLoc
13 import org.gradle.api.provider.Provider;
13 import org.gradle.api.provider.Provider;
14 import org.gradle.api.tasks.TaskProvider;
14 import org.gradle.api.tasks.TaskProvider;
15 import org.implab.gradle.common.core.lang.Deferred;
15 import org.implab.gradle.common.core.lang.Deferred;
16 import org.implab.gradle.internal.ReplayableQueue;
16 import org.implab.gradle.common.core.lang.ReplayableQueue;
17 import org.implab.gradle.variants.artifacts.ArtifactAssemblies;
17 import org.implab.gradle.variants.artifacts.ArtifactAssemblies;
18 import org.implab.gradle.variants.artifacts.ArtifactAssembly;
18 import org.implab.gradle.variants.artifacts.ArtifactAssembly;
19 import org.implab.gradle.variants.artifacts.ArtifactSlot;
19 import org.implab.gradle.variants.artifacts.ArtifactSlot;
@@ -15,14 +15,16 import org.gradle.api.artifacts.Configur
15 import org.gradle.api.model.ObjectFactory;
15 import org.gradle.api.model.ObjectFactory;
16 import org.gradle.api.provider.Property;
16 import org.gradle.api.provider.Property;
17 import org.implab.gradle.internal.IdentityContainerFactory;
17 import org.implab.gradle.internal.IdentityContainerFactory;
18 import org.implab.gradle.internal.ReplayableQueue;
18 import org.implab.gradle.common.core.lang.ReplayableQueue;
19 import org.implab.gradle.variants.artifacts.OutgoingVariant;
19 import org.implab.gradle.variants.artifacts.OutgoingVariant;
20 import org.implab.gradle.variants.artifacts.Slot;
20 import org.implab.gradle.variants.artifacts.Slot;
21 import org.implab.gradle.variants.core.Variant;
21 import org.implab.gradle.variants.core.Variant;
22
22
23 /**
23 /**
24 * Реестр исходящих вариантов. Связывает исходящие конфигурации с вариантами
24 * Registry of variant-level outgoing models.
25 * сборки. Связь устанавливается 1:1.
25 *
26 * <p>Each declared outgoing model owns one lazy Gradle consumable configuration
27 * and one live slot identity container.
26 */
28 */
27 @NonNullByDefault
29 @NonNullByDefault
28 public class OutgoingRegistry {
30 public class OutgoingRegistry {
@@ -66,9 +68,9 public class OutgoingRegistry {
66 }
68 }
67
69
68 /**
70 /**
69 * Replayable hook which is applied when an outgoing variant is defined
71 * Registers a replayable hook for outgoing variant declarations.
70 *
72 *
71 * @param action
73 * @param action outgoing variant action
72 */
74 */
73 public void configureEach(Consumer<? super OutgoingVariant> action) {
75 public void configureEach(Consumer<? super OutgoingVariant> action) {
74 outgoingVariants.forEach(action);
76 outgoingVariants.forEach(action);
@@ -2,10 +2,8 package org.implab.gradle.variants.sourc
2
2
3 import java.text.MessageFormat;
3 import java.text.MessageFormat;
4 import java.util.LinkedHashMap;
4 import java.util.LinkedHashMap;
5 import java.util.LinkedList;
6 import java.util.List;
5 import java.util.List;
7 import java.util.Map;
6 import java.util.Map;
8 import java.util.function.Consumer;
9 import java.util.function.Supplier;
7 import java.util.function.Supplier;
10 import java.util.stream.Collectors;
8 import java.util.stream.Collectors;
11
9
@@ -14,6 +12,7 import org.gradle.api.Action;
14 import org.gradle.api.Named;
12 import org.gradle.api.Named;
15 import org.gradle.api.logging.Logger;
13 import org.gradle.api.logging.Logger;
16 import org.gradle.api.logging.Logging;
14 import org.gradle.api.logging.Logging;
15 import org.implab.gradle.common.core.lang.ReplayableQueue;
17 import org.implab.gradle.variants.core.Layer;
16 import org.implab.gradle.variants.core.Layer;
18 import org.implab.gradle.variants.core.Variant;
17 import org.implab.gradle.variants.core.Variant;
19 import org.implab.gradle.variants.sources.CompileUnit;
18 import org.implab.gradle.variants.sources.CompileUnit;
@@ -57,7 +56,7 public class SourceSetConfigurationRegis
57 sourcesByUnit.computeIfAbsent(unit, key -> new ReplayableQueue<>()),
56 sourcesByUnit.computeIfAbsent(unit, key -> new ReplayableQueue<>()),
58 action,
57 action,
59 MessageFormat.format(
58 MessageFormat.format(
60 "Source set for [variant={0}, layer={1}] already materialed",
59 "Source set for [variant={0}, layer={1}] already materialized",
61 unit.variant().getName(),
60 unit.variant().getName(),
62 unit.layer().getName()));
61 unit.layer().getName()));
63 }
62 }
@@ -92,23 +91,4 public class SourceSetConfigurationRegis
92 sourcesByLayer.computeIfAbsent(unit.layer(), key -> new ReplayableQueue<>()).add(sourceSet);
91 sourcesByLayer.computeIfAbsent(unit.layer(), key -> new ReplayableQueue<>()).add(sourceSet);
93 sourcesByUnit.computeIfAbsent(unit, key -> new ReplayableQueue<>()).add(sourceSet);
92 sourcesByUnit.computeIfAbsent(unit, key -> new ReplayableQueue<>()).add(sourceSet);
94 }
93 }
95
96 class ReplayableQueue<T> {
97 private final List<Consumer<? super T>> consumers = new LinkedList<>();
98 private final List<T> values = new LinkedList<>();
99
100 public void add(T value) {
101 consumers.forEach(consumer -> consumer.accept(value));
102 values.add(value);
103 }
94 }
104
105 List<T> values() {
106 return List.copyOf(values);
107 }
108
109 public void forEach(Consumer<? super T> consumer) {
110 values.forEach(consumer);
111 consumers.add(consumer);
112 }
113 }
114 }
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now