Skip to content

Commit e7fef37

Browse files
feat: Config API to make sets deterministic (hiero-ledger#19735)
Signed-off-by: Anthony Petrov <anthony@swirldslabs.com>
1 parent 57ee35a commit e7fef37

File tree

4 files changed

+191
-13
lines changed

4 files changed

+191
-13
lines changed

platform-sdk/swirlds-config-api/src/main/java/com/swirlds/config/api/Configuration.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ <T> List<T> getValues(@NonNull String propertyName, @NonNull Class<T> propertyTy
150150

151151
/**
152152
* Returns a {@link Set} of string elements of the property with the given name.
153+
* The set has a stable iteration order.
153154
*
154155
* @param propertyName the name of the property
155156
* @return a {@link Set} of elements of the property with the given name
@@ -162,6 +163,7 @@ <T> List<T> getValues(@NonNull String propertyName, @NonNull Class<T> propertyTy
162163

163164
/**
164165
* Returns a {@link Set} of string elements of the property with the given name or the given default {@link Set}.
166+
* The set has a stable iteration order.
165167
*
166168
* @param propertyName the name of the property
167169
* @param defaultValue the default {@link Set}
@@ -174,6 +176,7 @@ <T> List<T> getValues(@NonNull String propertyName, @NonNull Class<T> propertyTy
174176

175177
/**
176178
* Returns a {@link Set} of elements of the property with the given name.
179+
* The set has a stable iteration order.
177180
*
178181
* @param propertyName the name of the property
179182
* @param propertyType the type of the elements
@@ -189,6 +192,7 @@ <T> Set<T> getValueSet(@NonNull String propertyName, @NonNull Class<T> propertyT
189192

190193
/**
191194
* Returns a {@link Set} of elements of the property with the given name or the given default {@link Set}.
195+
* The set has a stable iteration order.
192196
*
193197
* @param propertyName the name of the property
194198
* @param propertyType the type of the elements

platform-sdk/swirlds-config-impl/src/main/java/com/swirlds/config/impl/internal/ConfigDataFactory.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.lang.reflect.ParameterizedType;
1313
import java.lang.reflect.RecordComponent;
1414
import java.util.Arrays;
15+
import java.util.LinkedHashSet;
1516
import java.util.List;
1617
import java.util.Objects;
1718
import java.util.Optional;
@@ -143,7 +144,8 @@ private <T> Set<T> getDefaultValueSet(@NonNull final RecordComponent component)
143144
}
144145
return (Set<T>) ConfigListUtils.createList(rawValue).stream()
145146
.map(value -> converterService.convert(value, type))
146-
.collect(Collectors.toSet());
147+
// We want to retain the iteration order of items from the original list, so we use a LinkedHashSet:
148+
.collect(Collectors.toCollection(() -> new LinkedHashSet<>()));
147149
}
148150

149151
@SuppressWarnings("unchecked")

platform-sdk/swirlds-config-impl/src/main/java/com/swirlds/config/impl/internal/ConfigurationImpl.java

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
import edu.umd.cs.findbugs.annotations.NonNull;
77
import edu.umd.cs.findbugs.annotations.Nullable;
88
import java.util.Collection;
9+
import java.util.Collections;
10+
import java.util.EnumSet;
11+
import java.util.LinkedHashSet;
912
import java.util.List;
1013
import java.util.NoSuchElementException;
1114
import java.util.Objects;
1215
import java.util.Set;
16+
import java.util.TreeSet;
1317
import java.util.stream.Stream;
1418

1519
class ConfigurationImpl implements Configuration, ConfigLifecycle {
@@ -162,34 +166,52 @@ public <T> List<T> getValues(
162166
return getValues(propertyName, propertyType);
163167
}
164168

169+
/**
170+
* Creates an immutable set with a stable iteration order out of the provided collection.
171+
* @param collection a nullable collection
172+
* @param propertyType a non-null type of elements in the collection
173+
* @return null, or a set with a stable iteration order
174+
* @param <T> the type of elements in the set
175+
*/
176+
private <T> Set<T> createStableSet(@Nullable final Collection<T> collection, @NonNull final Class<T> propertyType) {
177+
if (collection == null) {
178+
return null;
179+
}
180+
if (collection.isEmpty()) {
181+
return Collections.emptySet();
182+
}
183+
if (Enum.class.isAssignableFrom(propertyType)) {
184+
return Collections.unmodifiableSet(EnumSet.copyOf((Collection<? extends Enum>) collection));
185+
}
186+
if (Comparable.class.isAssignableFrom(propertyType)) {
187+
return Collections.unmodifiableSortedSet(new TreeSet<>(collection));
188+
}
189+
// For non-sortable elements, we retain the order in which they're listed in the original collection
190+
return Collections.unmodifiableSet(new LinkedHashSet<>(collection));
191+
}
192+
165193
@Override
166194
@Nullable
167195
public Set<String> getValueSet(@NonNull final String propertyName) {
168196
final List<String> values = getValues(propertyName);
169-
if (values == null) {
170-
return null;
171-
}
172-
return Set.copyOf(values);
197+
return createStableSet(values, String.class);
173198
}
174199

175200
@Override
176201
@Nullable
177202
public Set<String> getValueSet(@NonNull final String propertyName, @Nullable final Set<String> defaultValue) {
178203
if (!exists(propertyName)) {
179-
return defaultValue;
204+
return createStableSet(defaultValue, String.class);
180205
}
181-
return getValueSet(propertyName);
206+
return createStableSet(getValues(propertyName), String.class);
182207
}
183208

184209
@Override
185210
@Nullable
186211
public <T> Set<T> getValueSet(@NonNull final String propertyName, @NonNull final Class<T> propertyType)
187212
throws NoSuchElementException, IllegalArgumentException {
188213
final List<T> values = getValues(propertyName, propertyType);
189-
if (values == null) {
190-
return null;
191-
}
192-
return Set.copyOf(values);
214+
return createStableSet(values, propertyType);
193215
}
194216

195217
@Override
@@ -200,9 +222,9 @@ public <T> Set<T> getValueSet(
200222
@Nullable final Set<T> defaultValue)
201223
throws IllegalArgumentException {
202224
if (!exists(propertyName)) {
203-
return defaultValue;
225+
return createStableSet(defaultValue, propertyType);
204226
}
205-
return getValueSet(propertyName, propertyType);
227+
return createStableSet(getValues(propertyName, propertyType), propertyType);
206228
}
207229

208230
@NonNull

platform-sdk/swirlds-config-impl/src/test/java/com/swirlds/config/impl/ConfigApiSetTests.java

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@
22
package com.swirlds.config.impl;
33

44
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertFalse;
6+
import static org.junit.jupiter.api.Assertions.assertNotNull;
57
import static org.junit.jupiter.api.Assertions.assertNull;
68
import static org.junit.jupiter.api.Assertions.assertThrows;
79
import static org.junit.jupiter.api.Assertions.assertTrue;
810

11+
import com.swirlds.config.api.ConfigData;
12+
import com.swirlds.config.api.ConfigProperty;
913
import com.swirlds.config.api.Configuration;
1014
import com.swirlds.config.api.ConfigurationBuilder;
1115
import com.swirlds.config.extensions.sources.SimpleConfigSource;
16+
import java.net.InetAddress;
17+
import java.net.UnknownHostException;
18+
import java.util.ArrayList;
19+
import java.util.Iterator;
1220
import java.util.List;
1321
import java.util.NoSuchElementException;
1422
import java.util.Set;
@@ -114,4 +122,146 @@ void testNotDefinedEmptySet() {
114122
assertThrows(NoSuchElementException.class, () -> configuration.getValueSet("sample.list", String.class));
115123
assertThrows(NoSuchElementException.class, () -> configuration.getValueSet("sample.list", Integer.class));
116124
}
125+
126+
/** Verify if the iteration order of the set is the same as the order of items in the list. */
127+
private static <T> void verifyIterationOrder(final Set<T> set, final List<T> list) {
128+
assertEquals(list.size(), set.size(), "The list size should be equal to the set size");
129+
130+
final Iterator<T> setIterator = set.iterator();
131+
final Iterator<T> listIterator = list.iterator();
132+
133+
final List<T> actualOrder = new ArrayList<>(list.size());
134+
135+
while (listIterator.hasNext()) {
136+
assertEquals(listIterator.hasNext(), setIterator.hasNext());
137+
138+
final T next = setIterator.next();
139+
actualOrder.add(next);
140+
141+
assertEquals(
142+
listIterator.next(),
143+
next,
144+
"The set iteration order should be stable. Expected: " + list + ", actual so far: " + actualOrder);
145+
}
146+
assertFalse(setIterator.hasNext());
147+
}
148+
149+
@Test
150+
void checkIntegerSetStable() {
151+
// given
152+
final Configuration configuration = ConfigurationBuilder.create()
153+
.withSource(new SimpleConfigSource("testNumbers", "3,1,2"))
154+
.build();
155+
156+
// when
157+
final Set<Integer> values = configuration.getValueSet("testNumbers", Integer.class);
158+
159+
// then
160+
verifyIterationOrder(values, List.of(1, 2, 3));
161+
}
162+
163+
@Test
164+
void checkStringSetStable() {
165+
// given
166+
final Configuration configuration = ConfigurationBuilder.create()
167+
.withSource(new SimpleConfigSource("testStrings", "x,a,d"))
168+
.build();
169+
170+
// when
171+
final Set<String> values = configuration.getValueSet("testStrings", String.class);
172+
173+
// then
174+
verifyIterationOrder(values, List.of("a", "d", "x"));
175+
}
176+
177+
@Test
178+
void checkEnumSetStable() {
179+
// given
180+
final Configuration configuration = ConfigurationBuilder.create()
181+
.withSource(new SimpleConfigSource("testEnums", "x,a,d"))
182+
.build();
183+
enum TestEnum {
184+
d,
185+
x,
186+
a
187+
}
188+
189+
// when
190+
final Set<TestEnum> values = configuration.getValueSet("testEnums", TestEnum.class);
191+
192+
// then
193+
verifyIterationOrder(values, List.of(TestEnum.d, TestEnum.x, TestEnum.a));
194+
}
195+
196+
@ConfigData("settest")
197+
public record SetTestConfig(@ConfigProperty(value = "testSet", defaultValue = "666,404,500") Set<Long> testSet) {}
198+
199+
@Test
200+
void checkSetInRecord() {
201+
// given
202+
final Configuration configuration = ConfigurationBuilder.create()
203+
.withSource(new SimpleConfigSource("settest.testSet", "333,111,222"))
204+
.withConfigDataType(SetTestConfig.class)
205+
.build();
206+
207+
// when
208+
final SetTestConfig setTestConfig = configuration.getConfigData(SetTestConfig.class);
209+
210+
// then
211+
verifyIterationOrder(setTestConfig.testSet(), List.of(111L, 222L, 333L));
212+
}
213+
214+
@Test
215+
void checkInetAddressSet() throws UnknownHostException {
216+
// InetAddress is not Comparable:
217+
218+
// given
219+
final Configuration configuration = ConfigurationBuilder.create()
220+
.withSource(new SimpleConfigSource("setinetaddresstest.testInetAddressSet", "1.1.1.1,2.2.2.2"))
221+
.build();
222+
223+
final List<InetAddress> expectedOrder = List.of(
224+
InetAddress.getByAddress(new byte[] {1, 1, 1, 1}), InetAddress.getByAddress(new byte[] {2, 2, 2, 2}));
225+
226+
// Case #1: as a List
227+
// when
228+
final List<InetAddress> list =
229+
configuration.getValues("setinetaddresstest.testInetAddressSet", InetAddress.class);
230+
231+
// then
232+
assertNotNull(list);
233+
assertEquals(2, list.size());
234+
assertEquals(expectedOrder, list);
235+
236+
// Case #2: as a Set
237+
// when/then
238+
final Set<InetAddress> set =
239+
configuration.getValueSet("setinetaddresstest.testInetAddressSet", InetAddress.class);
240+
verifyIterationOrder(set, expectedOrder);
241+
}
242+
243+
@ConfigData("setinetaddresstest")
244+
public record SetInetAddressTestConfig(
245+
@ConfigProperty(value = "testInetAddressSet", defaultValue = "1.1.1.1,2.2.2.2") Set<InetAddress> testSet) {}
246+
247+
@Test
248+
void checkInetAddressSetInRecord() throws UnknownHostException {
249+
final List<InetAddress> expectedOrder = List.of(
250+
InetAddress.getByAddress(new byte[] {1, 1, 1, 1}), InetAddress.getByAddress(new byte[] {2, 2, 2, 2}));
251+
252+
// InetAddress is not Comparable:
253+
final Configuration configuration = ConfigurationBuilder.create()
254+
.withSource(new SimpleConfigSource("setinetaddresstest.testInetAddressSet", "1.1.1.1,2.2.2.2"))
255+
.withConfigDataType(SetInetAddressTestConfig.class)
256+
.build();
257+
258+
// case 1: getValueSet
259+
final Set<InetAddress> set =
260+
configuration.getValueSet("setinetaddresstest.testInetAddressSet", InetAddress.class);
261+
verifyIterationOrder(set, expectedOrder);
262+
263+
// case 2: getConfigData as record
264+
final SetInetAddressTestConfig configData = configuration.getConfigData(SetInetAddressTestConfig.class);
265+
verifyIterationOrder(configData.testSet(), expectedOrder);
266+
}
117267
}

0 commit comments

Comments
 (0)