Skip to content

Commit d688e1b

Browse files
authored
[Java SDK] Warn when ValueState contains collection types (#37530)
When users declare ValueState<Map>, ValueState<List>, or ValueState<Set>, log a warning suggesting they use MapState, BagState, or SetState instead. Storing collections in ValueState requires reading and writing the entire collection on each access, which can cause performance issues for large collections. The specialized state types provide better performance.
1 parent c8aa28f commit d688e1b

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@
106106
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps;
107107
import org.checkerframework.checker.nullness.qual.Nullable;
108108
import org.joda.time.Instant;
109+
import org.slf4j.Logger;
110+
import org.slf4j.LoggerFactory;
109111

110112
/** Utilities for working with {@link DoFnSignature}. See {@link #getSignature}. */
111113
@Internal
@@ -115,6 +117,8 @@
115117
})
116118
public class DoFnSignatures {
117119

120+
private static final Logger LOG = LoggerFactory.getLogger(DoFnSignatures.class);
121+
118122
private DoFnSignatures() {}
119123

120124
/**
@@ -2368,12 +2372,57 @@ private static Map<String, DoFnSignature.StateDeclaration> analyzeStateDeclarati
23682372
(TypeDescriptor<? extends State>)
23692373
TypeDescriptor.of(fnClazz).resolveType(unresolvedStateType);
23702374

2375+
// Warn if ValueState contains a collection type that could benefit from specialized state
2376+
warnIfValueStateContainsCollection(fnClazz, id, stateType);
2377+
23712378
declarations.put(id, DoFnSignature.StateDeclaration.create(id, field, stateType));
23722379
}
23732380

23742381
return ImmutableMap.copyOf(declarations);
23752382
}
23762383

2384+
/**
2385+
* Warns if a ValueState is declared with a collection type (Map, List, Set) that could benefit
2386+
* from using specialized state types (MapState, BagState, SetState) for better performance.
2387+
*/
2388+
private static void warnIfValueStateContainsCollection(
2389+
Class<?> fnClazz, String stateId, TypeDescriptor<? extends State> stateType) {
2390+
if (!stateType.isSubtypeOf(TypeDescriptor.of(ValueState.class))) {
2391+
return;
2392+
}
2393+
2394+
// Use TypeDescriptor.resolveType() to extract ValueState's type parameter
2395+
// This preserves generic type information better than raw Type manipulation
2396+
TypeDescriptor<?> valueTypeDescriptor =
2397+
stateType.resolveType(ValueState.class.getTypeParameters()[0]);
2398+
2399+
// Match on the collection's raw type. We intentionally do not skip types with unresolved
2400+
// parameters: for a parameterized collection such as ValueState<Set<T>> the collection's own
2401+
// raw type (Set) is known even though the element type T is a type variable, so we can still
2402+
// recommend SetState. A payload that is itself a bare type variable (e.g. ValueState<T>) simply
2403+
// matches none of the branches below and produces no recommendation.
2404+
String recommendation = null;
2405+
if (valueTypeDescriptor.isSubtypeOf(TypeDescriptor.of(Map.class))) {
2406+
recommendation = "MapState";
2407+
} else if (valueTypeDescriptor.isSubtypeOf(TypeDescriptor.of(List.class))) {
2408+
recommendation = "BagState or OrderedListState";
2409+
} else if (valueTypeDescriptor.isSubtypeOf(TypeDescriptor.of(java.util.Set.class))) {
2410+
recommendation = "SetState";
2411+
}
2412+
2413+
if (recommendation != null) {
2414+
LOG.warn(
2415+
"DoFn {} declares ValueState '{}' with collection type {}. "
2416+
+ "ValueState reads/writes the entire collection on each access. "
2417+
+ "For large or frequently-updated collections, consider using {} instead "
2418+
+ "(if supported by your runner).",
2419+
fnClazz.getSimpleName(),
2420+
stateId,
2421+
valueTypeDescriptor.getRawType().getSimpleName(),
2422+
recommendation);
2423+
}
2424+
}
2425+
23772426
private static @Nullable Method findAnnotatedMethod(
23782427
ErrorReporter errors, Class<? extends Annotation> anno, Class<?> fnClazz, boolean required) {
23792428
Collection<Method> matches =

sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/reflect/DoFnSignaturesTest.java

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.apache.beam.sdk.state.TimerSpecs;
5252
import org.apache.beam.sdk.state.ValueState;
5353
import org.apache.beam.sdk.state.WatermarkHoldState;
54+
import org.apache.beam.sdk.testing.ExpectedLogs;
5455
import org.apache.beam.sdk.testing.SerializableMatchers;
5556
import org.apache.beam.sdk.transforms.DoFn;
5657
import org.apache.beam.sdk.transforms.Sum;
@@ -137,6 +138,8 @@ public void process(@FireTimestamp Instant ts) {}
137138

138139
@Rule public ExpectedException thrown = ExpectedException.none();
139140

141+
@Rule public ExpectedLogs expectedLogs = ExpectedLogs.none(DoFnSignatures.class);
142+
140143
@Test
141144
public void testBasicDoFnProcessContext() throws Exception {
142145
DoFnSignature sig =
@@ -1810,4 +1813,118 @@ public void onMyTimer() {}
18101813
@Override
18111814
public void processWithTimer(ProcessContext context, Timer timer) {}
18121815
}
1816+
1817+
// Test DoFns for ValueState collection warning tests
1818+
private static class DoFnWithMapValueState extends DoFn<String, String> {
1819+
@StateId("mapState")
1820+
private final StateSpec<ValueState<java.util.Map<String, String>>> mapState =
1821+
StateSpecs.value();
1822+
1823+
@ProcessElement
1824+
public void process() {}
1825+
}
1826+
1827+
private static class DoFnWithListValueState extends DoFn<String, String> {
1828+
@StateId("listState")
1829+
private final StateSpec<ValueState<java.util.List<String>>> listState = StateSpecs.value();
1830+
1831+
@ProcessElement
1832+
public void process() {}
1833+
}
1834+
1835+
private static class DoFnWithSetValueState extends DoFn<String, String> {
1836+
@StateId("setState")
1837+
private final StateSpec<ValueState<java.util.Set<String>>> setState = StateSpecs.value();
1838+
1839+
@ProcessElement
1840+
public void process() {}
1841+
}
1842+
1843+
private static class DoFnWithSimpleValueState extends DoFn<String, String> {
1844+
@StateId("simpleState")
1845+
private final StateSpec<ValueState<String>> simpleState = StateSpecs.value();
1846+
1847+
@ProcessElement
1848+
public void process() {}
1849+
}
1850+
1851+
private static class DoFnWithParameterizedSetValueState<T> extends DoFn<String, String> {
1852+
@StateId("parameterizedSetState")
1853+
private final StateSpec<ValueState<java.util.Set<T>>> parameterizedSetState =
1854+
StateSpecs.value();
1855+
1856+
@ProcessElement
1857+
public void process() {}
1858+
}
1859+
1860+
private static class DoFnWithParameterizedListValueState<T> extends DoFn<String, String> {
1861+
@StateId("parameterizedListState")
1862+
private final StateSpec<ValueState<java.util.List<T>>> parameterizedListState =
1863+
StateSpecs.value();
1864+
1865+
@ProcessElement
1866+
public void process() {}
1867+
}
1868+
1869+
private static class DoFnWithTypeVariableValueState<T> extends DoFn<String, String> {
1870+
@StateId("typeVariableState")
1871+
private final StateSpec<ValueState<T>> typeVariableState = StateSpecs.value();
1872+
1873+
@ProcessElement
1874+
public void process() {}
1875+
}
1876+
1877+
@Test
1878+
public void testValueStateWithMapLogsWarning() {
1879+
// This test verifies that the signature can be parsed for DoFns with collection ValueState.
1880+
// The warning is logged but doesn't prevent the signature from being created.
1881+
DoFnSignature signature = DoFnSignatures.getSignature(DoFnWithMapValueState.class);
1882+
assertThat(signature.stateDeclarations().get("mapState"), notNullValue());
1883+
}
1884+
1885+
@Test
1886+
public void testValueStateWithListLogsWarning() {
1887+
DoFnSignature signature = DoFnSignatures.getSignature(DoFnWithListValueState.class);
1888+
assertThat(signature.stateDeclarations().get("listState"), notNullValue());
1889+
}
1890+
1891+
@Test
1892+
public void testValueStateWithSetLogsWarning() {
1893+
DoFnSignature signature = DoFnSignatures.getSignature(DoFnWithSetValueState.class);
1894+
assertThat(signature.stateDeclarations().get("setState"), notNullValue());
1895+
}
1896+
1897+
@Test
1898+
public void testValueStateWithSimpleTypeNoWarning() {
1899+
// Simple types should not trigger any warning
1900+
DoFnSignature signature = DoFnSignatures.getSignature(DoFnWithSimpleValueState.class);
1901+
assertThat(signature.stateDeclarations().get("simpleState"), notNullValue());
1902+
expectedLogs.verifyNotLogged("with collection type");
1903+
}
1904+
1905+
@Test
1906+
public void testValueStateWithParameterizedSetLogsWarning() {
1907+
// A parameterized collection such as ValueState<Set<T>> still has a known raw collection type
1908+
// (Set) even though the element type is a type variable, so it should still recommend SetState.
1909+
DoFnSignature signature = DoFnSignatures.getSignature(DoFnWithParameterizedSetValueState.class);
1910+
assertThat(signature.stateDeclarations().get("parameterizedSetState"), notNullValue());
1911+
expectedLogs.verifyWarn("SetState");
1912+
}
1913+
1914+
@Test
1915+
public void testValueStateWithParameterizedListLogsWarning() {
1916+
DoFnSignature signature =
1917+
DoFnSignatures.getSignature(DoFnWithParameterizedListValueState.class);
1918+
assertThat(signature.stateDeclarations().get("parameterizedListState"), notNullValue());
1919+
expectedLogs.verifyWarn("BagState or OrderedListState");
1920+
}
1921+
1922+
@Test
1923+
public void testValueStateWithBareTypeVariableNoWarning() {
1924+
// A bare type variable payload (ValueState<T>) has no raw collection type to inspect and must
1925+
// not produce a collection recommendation.
1926+
DoFnSignature signature = DoFnSignatures.getSignature(DoFnWithTypeVariableValueState.class);
1927+
assertThat(signature.stateDeclarations().get("typeVariableState"), notNullValue());
1928+
expectedLogs.verifyNotLogged("with collection type");
1929+
}
18131930
}

0 commit comments

Comments
 (0)