Skip to content

Commit 8baf5de

Browse files
committed
refactor(inkless): SharedState to own StorageBackends
Properly separating storage back-end clients between read/write and background jobs. SharedState now owns the StorageBackends lifecycle, properly closing all of them. So, Reader and Writer component do not have to close storages anymore. Alongside, making background components internal constructors visible for easier testing. This allows to make internal SharedState constructor private.
1 parent d6c4996 commit 8baf5de

File tree

21 files changed

+450
-294
lines changed

21 files changed

+450
-294
lines changed

core/src/test/scala/unit/kafka/server/ReplicaManagerTest.scala

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ import org.mockito.ArgumentMatchers._
9191
import org.mockito.Mockito._
9292
import org.mockito.invocation.InvocationOnMock
9393
import org.mockito.stubbing.Answer
94-
import org.mockito.{Answers, ArgumentCaptor, ArgumentMatchers, MockedConstruction}
94+
import org.mockito.{ArgumentCaptor, ArgumentMatchers, MockedConstruction}
9595

9696
import java.io.{ByteArrayInputStream, File}
9797
import java.net.InetAddress
@@ -6495,7 +6495,7 @@ class ReplicaManagerTest {
64956495
}
64966496

64976497
@Test
6498-
def testAppendWithInvalidDisklessAndValidCLassic(): Unit = {
6498+
def testAppendWithInvalidDisklessAndValidClassic(): Unit = {
64996499
val entriesPerPartition = Map(
65006500
disklessTopicPartition -> RECORDS,
65016501
classicTopicPartition -> RECORDS,
@@ -6643,7 +6643,7 @@ class ReplicaManagerTest {
66436643
val replicaManager = try {
66446644
createReplicaManager(
66456645
List(disklessTopicPartition.topic(), disklessTopicPartition2.topic()),
6646-
controlPlane = Some(cp),
6646+
controlPlaneOption = Some(cp),
66476647
topicIdMapping = Map(disklessTopicPartition2.topic() -> disklessTopicPartition2.topicId())
66486648
)
66496649
} finally {
@@ -6706,7 +6706,7 @@ class ReplicaManagerTest {
67066706
val fetchHandlerCtor: MockedConstruction[FetchHandler] = mockFetchHandler(disklessResponse)
67076707

67086708
val replicaManager = try {
6709-
createReplicaManager(List(disklessTopicPartition.topic()), controlPlane = Some(cp))
6709+
createReplicaManager(List(disklessTopicPartition.topic()), controlPlaneOption = Some(cp))
67106710
} finally {
67116711
fetchHandlerCtor.close()
67126712
}
@@ -6765,7 +6765,7 @@ class ReplicaManagerTest {
67656765

67666766
val replicaManager = try {
67676767
// spy to inject readFromLog mock
6768-
spy(createReplicaManager(List(disklessTopicPartition.topic()), controlPlane = Some(cp)))
6768+
spy(createReplicaManager(List(disklessTopicPartition.topic()), controlPlaneOption = Some(cp)))
67696769
} finally {
67706770
fetchHandlerCtor.close()
67716771
}
@@ -6845,7 +6845,7 @@ class ReplicaManagerTest {
68456845

68466846
val replicaManager = try {
68476847
// spy to inject readFromLog mock
6848-
spy(createReplicaManager(List(disklessTopicPartition.topic()), controlPlane = Some(cp)))
6848+
spy(createReplicaManager(List(disklessTopicPartition.topic()), controlPlaneOption = Some(cp)))
68496849
} finally {
68506850
fetchHandlerCtor.close()
68516851
}
@@ -6919,7 +6919,7 @@ class ReplicaManagerTest {
69196919
val fetchHandlerCtor: MockedConstruction[FetchHandler] = mockFetchHandler(disklessResponse)
69206920

69216921
val replicaManager = try {
6922-
createReplicaManager(List(disklessTopicPartition.topic()), controlPlane = Some(cp))
6922+
createReplicaManager(List(disklessTopicPartition.topic()), controlPlaneOption = Some(cp))
69236923
} finally {
69246924
fetchHandlerCtor.close()
69256925
}
@@ -6982,7 +6982,7 @@ class ReplicaManagerTest {
69826982
val fetchHandlerCtor: MockedConstruction[FetchHandler] = mockFetchHandler(disklessResponse)
69836983

69846984
val replicaManager = try {
6985-
spy(createReplicaManager(List(disklessTopicPartition.topic()), controlPlane = Some(cp)))
6985+
spy(createReplicaManager(List(disklessTopicPartition.topic()), controlPlaneOption = Some(cp)))
69866986
} finally {
69876987
fetchHandlerCtor.close()
69886988
}
@@ -7057,7 +7057,7 @@ class ReplicaManagerTest {
70577057
val fetchHandlerCtor: MockedConstruction[FetchHandler] = mockFetchHandler(disklessResponse)
70587058

70597059
val replicaManager = try {
7060-
spy(createReplicaManager(List(disklessTopicPartition.topic()), controlPlane = Some(cp)))
7060+
spy(createReplicaManager(List(disklessTopicPartition.topic()), controlPlaneOption = Some(cp)))
70617061
} finally {
70627062
fetchHandlerCtor.close()
70637063
}
@@ -7130,24 +7130,25 @@ class ReplicaManagerTest {
71307130

71317131
private def createReplicaManager(
71327132
disklessTopics: Seq[String],
7133-
controlPlane: Option[ControlPlane] = None,
7133+
controlPlaneOption: Option[ControlPlane] = None,
71347134
topicIdMapping: Map[String, Uuid] = Map.empty
71357135
): ReplicaManager = {
7136-
val props = TestUtils.createBrokerConfig(1, logDirCount = 2)
7136+
val brokerId = 1
7137+
val props = TestUtils.createBrokerConfig(brokerId, logDirCount = 2)
71377138
val config = KafkaConfig.fromProps(props)
71387139
val mockLogMgr = TestUtils.createLogManager(config.logDirs.asScala.map(new File(_)), new LogConfig(new Properties()))
7139-
val sharedState = mock(classOf[SharedState], Answers.RETURNS_DEEP_STUBS)
7140-
when(sharedState.time()).thenReturn(Time.SYSTEM)
7141-
when(sharedState.config()).thenReturn(new InklessConfig(new util.HashMap[String, Object]()))
7142-
when(sharedState.controlPlane()).thenReturn(controlPlane.getOrElse(mock(classOf[ControlPlane])))
7140+
val inklessConfigProps = new util.HashMap[String, Object]()
7141+
inklessConfigProps.put(InklessConfig.CONSUME_BATCH_COORDINATE_CACHE_ENABLED_CONFIG, java.lang.Boolean.FALSE)
7142+
val inklessConfig = new InklessConfig(inklessConfigProps)
7143+
val controlPlane = controlPlaneOption.getOrElse(mock(classOf[ControlPlane]))
71437144
val inklessMetadata = mock(classOf[MetadataView])
71447145
when(inklessMetadata.isDisklessTopic(any())).thenReturn(false)
71457146
when(inklessMetadata.getTopicId(anyString())).thenAnswer{ invocation =>
71467147
val topicName = invocation.getArgument(0, classOf[String])
71477148
topicIdMapping.getOrElse(topicName, Uuid.ZERO_UUID)
71487149
}
71497150
disklessTopics.foreach(t => when(inklessMetadata.isDisklessTopic(t)).thenReturn(true))
7150-
when(sharedState.metadata()).thenReturn(inklessMetadata)
7151+
val sharedState = SharedState.initialize(time, brokerId, inklessConfig, inklessMetadata, controlPlane, new BrokerTopicStats(), () => new LogConfig(new Properties()))
71517152

71527153
val logDirFailureChannel = new LogDirFailureChannel(config.logDirs.size)
71537154

storage/inkless/src/main/java/io/aiven/inkless/common/SharedState.java

Lines changed: 120 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@
2323
import org.apache.kafka.common.metrics.Metrics;
2424
import org.apache.kafka.common.metrics.MetricsReporter;
2525
import org.apache.kafka.common.utils.Time;
26+
import org.apache.kafka.common.utils.Utils;
2627
import org.apache.kafka.storage.internals.log.LogConfig;
2728
import org.apache.kafka.storage.log.metrics.BrokerTopicStats;
2829

2930
import java.io.Closeable;
3031
import java.io.IOException;
3132
import java.time.Duration;
3233
import java.util.List;
34+
import java.util.Optional;
3335
import java.util.function.Supplier;
3436

3537
import io.aiven.inkless.cache.BatchCoordinateCache;
@@ -52,6 +54,18 @@ public final class SharedState implements Closeable {
5254
private final InklessConfig config;
5355
private final MetadataView metadata;
5456
private final ControlPlane controlPlane;
57+
private final StorageBackend fetchStorage;
58+
// Separate storage client for lagging consumers to:
59+
// 1. Isolate connection pool usage (lagging consumers shouldn't exhaust connections for hot path)
60+
// 2. Allow independent tuning of timeouts/retries for cold storage access patterns
61+
// (This requires some refactoring on how the storage client is built/configured)
62+
private final Optional<StorageBackend> maybeLaggingFetchStorage;
63+
private final StorageBackend produceStorage;
64+
// backgroundStorage is shared by FileCleaner and FileMerger executors which run concurrently.
65+
// Kafka storage backends are required to be thread-safe (they share the same Metrics instance).
66+
// However, these tasks perform high-latency object storage calls and retries. A dedicated backend
67+
// instance guarantees they don't contend with hot-path fetch/produce clients and prevents threading/double-close issues.
68+
private final StorageBackend backgroundStorage;
5569
private final ObjectKeyCreator objectKeyCreator;
5670
private final KeyAlignmentStrategy keyAlignmentStrategy;
5771
private final ObjectCache cache;
@@ -60,12 +74,17 @@ public final class SharedState implements Closeable {
6074
private final Supplier<LogConfig> defaultTopicConfigs;
6175
private final Metrics storageMetrics;
6276

63-
public SharedState(
77+
private SharedState(
6478
final Time time,
6579
final int brokerId,
6680
final InklessConfig config,
6781
final MetadataView metadata,
6882
final ControlPlane controlPlane,
83+
final StorageBackend fetchStorage,
84+
final Optional<StorageBackend> maybeLaggingFetchStorage,
85+
final StorageBackend produceStorage,
86+
final StorageBackend backgroundStorage,
87+
final Metrics storageMetrics,
6988
final ObjectKeyCreator objectKeyCreator,
7089
final KeyAlignmentStrategy keyAlignmentStrategy,
7190
final ObjectCache cache,
@@ -84,12 +103,11 @@ public SharedState(
84103
this.batchCoordinateCache = batchCoordinateCache;
85104
this.brokerTopicStats = brokerTopicStats;
86105
this.defaultTopicConfigs = defaultTopicConfigs;
87-
88-
final MetricsReporter reporter = new JmxReporter();
89-
this.storageMetrics = new Metrics(
90-
new MetricConfig(), List.of(reporter), Time.SYSTEM,
91-
new KafkaMetricsContext(STORAGE_METRIC_CONTEXT)
92-
);
106+
this.fetchStorage = fetchStorage;
107+
this.maybeLaggingFetchStorage = maybeLaggingFetchStorage;
108+
this.produceStorage = produceStorage;
109+
this.backgroundStorage = backgroundStorage;
110+
this.storageMetrics = storageMetrics;
93111
}
94112

95113
public static SharedState initialize(
@@ -107,34 +125,88 @@ public static SharedState initialize(
107125
"Value of consume.batch.coordinate.cache.ttl.ms exceeds file.cleaner.retention.period.ms / 2"
108126
);
109127
}
110-
return new SharedState(
111-
time,
112-
brokerId,
113-
config,
114-
metadata,
115-
controlPlane,
116-
ObjectKey.creator(config.objectKeyPrefix(), config.objectKeyLogPrefixMasked()),
117-
new FixedBlockAlignment(config.fetchCacheBlockBytes()),
118-
new CaffeineCache(
128+
129+
CaffeineCache objectCache = null;
130+
BatchCoordinateCache batchCoordinateCache = null;
131+
StorageBackend fetchStorage = null;
132+
StorageBackend laggingFetchStorage = null;
133+
StorageBackend produceStorage = null;
134+
StorageBackend backgroundStorage = null;
135+
Metrics storageMetrics = null;
136+
try {
137+
objectCache = new CaffeineCache(
119138
config.cacheMaxCount(),
120139
config.cacheExpirationLifespanSec(),
121140
config.cacheExpirationMaxIdleSec()
122-
),
123-
config.isBatchCoordinateCacheEnabled() ? new CaffeineBatchCoordinateCache(config.batchCoordinateCacheTtl()) : new NullBatchCoordinateCache(),
124-
brokerTopicStats,
125-
defaultTopicConfigs
126-
);
141+
);
142+
batchCoordinateCache = config.isBatchCoordinateCacheEnabled()
143+
? new CaffeineBatchCoordinateCache(config.batchCoordinateCacheTtl())
144+
: new NullBatchCoordinateCache();
145+
146+
final MetricsReporter reporter = new JmxReporter();
147+
storageMetrics = new Metrics(
148+
new MetricConfig(), List.of(reporter), Time.SYSTEM,
149+
new KafkaMetricsContext(STORAGE_METRIC_CONTEXT)
150+
);
151+
fetchStorage = config.storage(storageMetrics);
152+
// If thread pool size is 0, disabling lagging consumer support, don't create a separate client
153+
//
154+
// NOTE: The client for lagging consumers is created only when this SharedState
155+
// is constructed. If fetchLaggingConsumerThreadPoolSize() is 0 at this time, no separate
156+
// client is created and lagging consumer support is effectively disabled for the lifetime
157+
// of this instance, even if the configuration is later reloaded with a non-zero value.
158+
// Enabling lagging consumer support therefore requires a broker restart (or reconstruction
159+
// of the SharedState) so that a new storage client can be created.
160+
laggingFetchStorage = config.fetchLaggingConsumerThreadPoolSize() > 0 ? config.storage(storageMetrics) : null;
161+
produceStorage = config.storage(storageMetrics);
162+
backgroundStorage = config.storage(storageMetrics);
163+
final var objectKeyCreator = ObjectKey.creator(config.objectKeyPrefix(), config.objectKeyLogPrefixMasked());
164+
final var keyAlignmentStrategy = new FixedBlockAlignment(config.fetchCacheBlockBytes());
165+
return new SharedState(
166+
time,
167+
brokerId,
168+
config,
169+
metadata,
170+
controlPlane,
171+
fetchStorage,
172+
Optional.ofNullable(laggingFetchStorage),
173+
produceStorage,
174+
backgroundStorage,
175+
storageMetrics,
176+
objectKeyCreator,
177+
keyAlignmentStrategy,
178+
objectCache,
179+
batchCoordinateCache,
180+
brokerTopicStats,
181+
defaultTopicConfigs
182+
);
183+
} catch (Exception e) {
184+
Utils.closeQuietly(backgroundStorage, "backgroundStorage");
185+
Utils.closeQuietly(produceStorage, "produceStorage");
186+
Utils.closeQuietly(fetchStorage, "fetchStorage");
187+
Utils.closeQuietly(laggingFetchStorage, "laggingFetchStorage");
188+
Utils.closeQuietly(batchCoordinateCache, "batchCoordinateCache");
189+
Utils.closeQuietly(objectCache, "objectCache");
190+
Utils.closeQuietly(storageMetrics, "storageMetrics");
191+
192+
throw new RuntimeException("Failed to initialize SharedState", e);
193+
}
127194
}
128195

129196
@Override
130197
public void close() throws IOException {
131-
try {
132-
cache.close();
133-
controlPlane.close();
134-
storageMetrics.close();
135-
} catch (Exception e) {
136-
throw new RuntimeException(e);
137-
}
198+
// Closing storage backends
199+
Utils.closeQuietly(backgroundStorage, "backgroundStorage");
200+
Utils.closeQuietly(produceStorage, "produceStorage");
201+
maybeLaggingFetchStorage.ifPresent(s -> Utils.closeQuietly(s, "laggingFetchStorage"));
202+
Utils.closeQuietly(fetchStorage, "fetchStorage");
203+
// Closing storage metrics
204+
Utils.closeQuietly(storageMetrics, "storageMetrics");
205+
// Closing caches
206+
Utils.closeQuietly(cache, "objectCache");
207+
Utils.closeQuietly(batchCoordinateCache, "batchCoordinateCache");
208+
// Closing control plane
209+
Utils.closeQuietly(controlPlane, "controlPlane");
138210
}
139211

140212
public Time time() {
@@ -185,7 +257,25 @@ public Supplier<LogConfig> defaultTopicConfigs() {
185257
return defaultTopicConfigs;
186258
}
187259

188-
public StorageBackend buildStorage() {
189-
return config.storage(storageMetrics);
260+
public StorageBackend fetchStorage() {
261+
return fetchStorage;
262+
}
263+
264+
/**
265+
* Optional access to the lagging fetch storage backend.
266+
*
267+
* <p>When {@code fetch.lagging.consumer.thread.pool.size == 0}, the lagging consumer
268+
* path is explicitly disabled and this storage backend is not created.</p>
269+
*/
270+
public Optional<StorageBackend> maybeLaggingFetchStorage() {
271+
return maybeLaggingFetchStorage;
272+
}
273+
274+
public StorageBackend produceStorage() {
275+
return produceStorage;
276+
}
277+
278+
public StorageBackend backgroundStorage() {
279+
return backgroundStorage;
190280
}
191281
}

storage/inkless/src/main/java/io/aiven/inkless/consume/FetchHandler.java

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,23 +51,11 @@ public FetchHandler(final SharedState state) {
5151
state.keyAlignmentStrategy(),
5252
state.cache(),
5353
state.controlPlane(),
54-
state.buildStorage(),
54+
state.fetchStorage(),
5555
state.brokerTopicStats(),
5656
state.config().fetchMetadataThreadPoolSize(),
5757
state.config().fetchDataThreadPoolSize(),
58-
// Separate storage client for lagging consumers to:
59-
// 1. Isolate connection pool usage (lagging consumers shouldn't exhaust connections for hot path)
60-
// 2. Allow independent tuning of timeouts/retries for cold storage access patterns
61-
// (This requires some refactoring on how the storage client is built/configured)
62-
// If thread pool size is 0, disabling lagging consumer support, don't create a separate client
63-
//
64-
// NOTE: The client for lagging consumers is created only when this FetchHandler (and Reader)
65-
// is constructed. If fetchLaggingConsumerThreadPoolSize() is 0 at this time, no separate
66-
// client is created and lagging consumer support is effectively disabled for the lifetime
67-
// of this instance, even if the configuration is later reloaded with a non-zero value.
68-
// Enabling lagging consumer support therefore requires a broker restart (or reconstruction
69-
// of the SharedState/FetchHandler) so that a new storage client can be created.
70-
state.config().fetchLaggingConsumerThreadPoolSize() > 0 ? state.buildStorage() : null,
58+
state.maybeLaggingFetchStorage(),
7159
state.config().fetchLaggingConsumerThresholdMs(),
7260
state.config().fetchLaggingConsumerRequestRateLimit(),
7361
state.config().fetchLaggingConsumerThreadPoolSize(),
@@ -76,7 +64,8 @@ public FetchHandler(final SharedState state) {
7664
);
7765
}
7866

79-
public FetchHandler(final Reader reader) {
67+
// Visible for testing
68+
FetchHandler(final Reader reader) {
8069
this.reader = reader;
8170
}
8271

0 commit comments

Comments
 (0)