Skip to content

Commit b435bc9

Browse files
committed
Merge remote-tracking branch 'apache/master' into mmerli/ml-ledger-ordering-key
# Conflicts: # managed-ledger/src/test/java/org/apache/bookkeeper/mledger/impl/cache/RangeEntryCacheImplTest.java
2 parents f17093e + 14e0ede commit b435bc9

90 files changed

Lines changed: 4363 additions & 1043 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTRIBUTING.md

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -283,56 +283,11 @@ disallows privileged containers (the run continues either way, with less accurat
283283
`-Pdocker.wolfi` builds the base image from Wolfi, which is what makes the `GLIBC_TUNABLES` the test
284284
sets take effect.
285285

286-
#### Analyzing a JFR file
286+
#### Performance recording analysis
287287

288-
A recording collects several events at once, and each of them is worth looking at from more than one
289-
angle, so **`jfrFlamegraphs`** renders the lot in one go:
290-
291-
```bash
292-
./gradlew jfrFlamegraphs # every .jfr in build/test-profiles
293-
./gradlew jfrFlamegraphs -Pjfr=tests/integration/build # the integration-test recordings
294-
./gradlew jfrFlamegraphs -Pjfr=<some-file>.jfr # just one recording
295-
```
296-
297-
Each recording gets a directory beside it named after the file without its extension plus a
298-
`-flamegraphs` suffix — `profile.jfr` produces `profile-flamegraphs/` — holding one flame graph per
299-
view: `cpu`, `wall`, `alloc` and `lock`, each rendered merged (`cpu.html`), split per thread
300-
(`cpu_threads.html`) and grouped into async-profiler's categories (`cpu_classify.html`). A view whose
301-
event the recording does not contain is skipped rather than failing the run. Pass
302-
`-Pjfr.types=cpu,nativemem` to render a different set of events, and `-Pjfrconv=<path>` when
303-
async-profiler's `jfrconv` is neither next to the library `LIBASYNCPROFILER_PATH` points at nor in
304-
the JDK that runs Gradle (Amazon Corretto ships it).
305-
306-
For anything the flame graphs don't answer, open the `.jfr` itself in
307-
[Eclipse Mission Control](https://adoptium.net/jmc) or IntelliJ IDEA, or run `jfrconv` by hand.
308-
309-
> Do not judge a recording by `jfr summary` (the JDK's own tool). The default options record through
310-
> `jfrsync`, and `jfr summary` reports only a handful of `jdk.ExecutionSample` events for a recording
311-
> that `jfrconv` reads hundreds of profiler samples from — it looks empty when it is not.
312-
313-
#### Agent-assisted analysis with the Jafar MCP server
314-
315-
The [Jafar MCP server](https://github.com/btraceio/jafar/blob/main/jfr-mcp/README.md) lets an AI
316-
coding agent read a JFR recording directly, which turns a flame graph into something you can ask
317-
questions about. Register it once (it needs [JBang](https://www.jbang.dev/) and JDK 25+):
318-
319-
```bash
320-
claude mcp add jafar -- jbang jfr-mcp@btraceio --stdio
321-
```
322-
323-
It exposes `jfr_diagnose` (automated diagnosis of a recording), `jfr_stackprofile` (structured stack
324-
profiling with a time-series and per-thread breakdown), `jfr_hotmethods`, `jfr_flamegraph`,
325-
`jfr_callgraph`, `jfr_exceptions`, `jfr_tsa` (thread-state analysis), `jfr_use` (USE method) and
326-
`jfr_query` for [JfrPath](https://github.com/btraceio/jafar) queries. A prompt that works well as a
327-
starting point:
328-
329-
> use Jafar MCP's jfr_diagnose and jfr_stackprofile to analyze @filename.jfr. Besides showing the
330-
> report on the console, write the analysis in a markdown file with the jfr file as prefix and the
331-
> suffix as ".analysis.md"
332-
333-
Treat the result as a lead to verify, not a conclusion: the
334-
[agent guardrails](AGENTS.md) apply here as much as anywhere, and a performance claim still needs a
335-
benchmark or a second profile behind it.
288+
See [`tests/performance/README.md`](tests/performance/README.md) for JFR rendering, async-profiler
289+
recording analysis, Jafar MCP usage and MAT MCP based memory-leak investigation. That document also
290+
explains how to keep the raw recordings and analysis next to the workload documentation.
336291

337292
### Integration tests
338293

conf/broker.conf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,6 +1481,18 @@ managedLedgerCacheEvictionExtendTTLOfEntriesWithRemainingExpectedReadsMaxTimes=5
14811481
# Default is true, to behave like a LRU cache.
14821482
managedLedgerCacheEvictionExtendTTLOfRecentlyAccessed=true
14831483

1484+
# Enable the BookKeeper batch read API for managed-ledger storage reads: a single RPC fetches
1485+
# multiple entries, reducing network overhead for sequential reads. Batch read requires
1486+
# bookkeeperUseV2WireProtocol and BookKeeper's own batch read flag (bookkeeper_batchReadEnabled),
1487+
# checked on the BookKeeper client when a topic is loaded: regular reads are used otherwise, as
1488+
# well as for striped ledgers (managedLedgerDefaultEnsembleSize differs from
1489+
# managedLedgerDefaultWriteQuorum) and for bookies without batch read support. Each batch read
1490+
# request is bounded by the size limit of the dispatcher read that triggered it (e.g.
1491+
# dispatcherMaxReadSizeBytes) and by the BookKeeper client's max frame size (maxMessageSize plus
1492+
# padding); a read needing more data is split into sequential batch read requests. Entries read
1493+
# this way are copied when inserted in the entry cache.
1494+
managedLedgerBatchReadEnabled=true
1495+
14841496
# This setting configures the duration of continuing to cache added entries while there are no
14851497
# active cursors, when the last active cursor has left or immediately after initialization when
14861498
# the persistent topic and the managed ledger gets loaded.

conf/pulsar_env.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@
4949
# pooled - Netty PooledByteBufAllocator; prefers direct buffers (default).
5050
# unpooled - Netty UnpooledByteBufAllocator; prefers heap buffers.
5151
# adaptive - Netty AdaptiveByteBufAllocator; auto-tunes pooling and prefers direct buffers.
52+
# Named allocators override each setting independently with pulsar.allocator.<id>.<setting>:
53+
# pulsar.allocator.default.type - allocator used by general Pulsar operations
54+
# pulsar.allocator.ml-cache.type - separate allocator used for managed-ledger cache copies (default: adaptive)
55+
# Supported settings: type, exit_on_oom (false), out_of_memory_policy (FallbackToHeap or ThrowException).
56+
# Named settings fall back to the unqualified pulsar.allocator.<setting>, then the built-in default.
57+
# Explicit global type and legacy pooled settings also apply to ml-cache unless overridden by name.
58+
# Batch reads copy entries into this cache even when managedLedgerCacheCopyEntries=false. Adaptive
59+
# reuses small size-class slots to limit fragmentation; retained chunks and size rounding still cost memory.
60+
# To retain the previous cache allocator: -Dpulsar.allocator.ml-cache.type=pooled
61+
# Settings are read when an allocator is first created. default overrides do not apply to other IDs.
62+
# Leak detection is global: use -Dio.netty.leakDetection.level=disabled|simple|advanced|paranoid.
63+
# pulsar.allocator.leak_detection and per-allocator leak_detection settings are not supported.
5264
# -Dpulsar.allocator.pooled=true is deprecated; use -Dpulsar.allocator.type=pooled instead.
5365
# pulsar.allocator.type takes precedence over the legacy pulsar.allocator.pooled property.
5466
# If pulsar.allocator.type is unset, pulsar.allocator.pooled=true (or unset) selects pooled;

conf/standalone.conf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,18 @@ managedLedgerCacheEvictionExtendTTLOfEntriesWithRemainingExpectedReadsMaxTimes=5
981981
# Default is true, to behave like a LRU cache.
982982
managedLedgerCacheEvictionExtendTTLOfRecentlyAccessed=true
983983

984+
# Enable the BookKeeper batch read API for managed-ledger storage reads: a single RPC fetches
985+
# multiple entries, reducing network overhead for sequential reads. Batch read requires
986+
# bookkeeperUseV2WireProtocol and BookKeeper's own batch read flag (bookkeeper_batchReadEnabled),
987+
# checked on the BookKeeper client when a topic is loaded: regular reads are used otherwise, as
988+
# well as for striped ledgers (managedLedgerDefaultEnsembleSize differs from
989+
# managedLedgerDefaultWriteQuorum) and for bookies without batch read support. Each batch read
990+
# request is bounded by the size limit of the dispatcher read that triggered it (e.g.
991+
# dispatcherMaxReadSizeBytes) and by the BookKeeper client's max frame size (maxMessageSize plus
992+
# padding); a read needing more data is split into sequential batch read requests. Entries read
993+
# this way are copied when inserted in the entry cache.
994+
managedLedgerBatchReadEnabled=true
995+
984996
# This setting configures the duration of continuing to cache added entries while there are no
985997
# active cursors, when the last active cursor has left or immediately after initialization when
986998
# the persistent topic and the managed ledger gets loaded.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.bookkeeper.client;
20+
21+
import org.apache.bookkeeper.conf.ClientConfiguration;
22+
23+
/**
24+
* Gives the managed ledger access to what a {@link BookKeeper} client's configuration says about the client, whose
25+
* accessor is protected: this class lives in the client's package for that purpose.
26+
*/
27+
public final class BookKeeperClientConfigAccessor {
28+
29+
private BookKeeperClientConfigAccessor() {
30+
}
31+
32+
/**
33+
* Whether the client can issue batch reads: they require the v2 wire protocol and the client's own batch read
34+
* flag. A client without a configuration (a mock) cannot.
35+
*/
36+
public static boolean supportsBatchRead(BookKeeper bookKeeper) {
37+
ClientConfiguration conf = bookKeeper.getConf();
38+
return conf != null && conf.getUseV2WireProtocol() && conf.isBatchReadEnabled();
39+
}
40+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
/**
20+
* Managed ledger access to BookKeeper client internals that the client does not expose outside its package.
21+
*/
22+
package org.apache.bookkeeper.client;

managed-ledger/src/main/java/org/apache/bookkeeper/mledger/ManagedLedgerConfig.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,20 @@ public class ManagedLedgerConfig {
8888
@Getter
8989
@Setter
9090
private boolean cacheEvictionByExpectedReadCount = true;
91+
92+
/**
93+
* Enable the BookKeeper batch read API when reading entries from bookkeeper: a single RPC fetches multiple
94+
* entries, reducing network overhead. It is only used when the BookKeeper client supports it (v2 wire protocol
95+
* with batch reads enabled), which the managed ledger checks when it is opened; the client uses regular reads for
96+
* striped ledgers (ensembleSize differs from writeQuorumSize) and for bookies without batch read support. Each
97+
* batch read request is bounded by the size limit of the read that triggered it and by the client's max frame
98+
* size, a read needing more data being split into sequential requests. Entries read this way are copied when
99+
* inserted in the entry cache, since their buffers are slices of a shared response frame.
100+
*/
101+
@Getter
102+
@Setter
103+
private boolean batchReadEnabled = true;
104+
91105
/**
92106
* Whether the entries of this managed ledger are Pulsar messages, so that an entry's payload begins with the
93107
* headers that parse into a {@code MessageMetadata}.

managed-ledger/src/main/java/org/apache/bookkeeper/mledger/OpenTelemetryManagedLedgerCacheStats.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@
1818
*/
1919
package org.apache.bookkeeper.mledger;
2020

21+
import static org.apache.pulsar.common.allocator.PulsarByteBufAllocator.ML_CACHE_ALLOCATOR_NAME;
2122
import io.opentelemetry.api.OpenTelemetry;
2223
import io.opentelemetry.api.metrics.BatchCallback;
2324
import io.opentelemetry.api.metrics.ObservableLongMeasurement;
2425
import org.apache.bookkeeper.mledger.impl.ManagedLedgerFactoryImpl;
25-
import org.apache.bookkeeper.mledger.impl.cache.PooledByteBufAllocatorStats;
26-
import org.apache.bookkeeper.mledger.impl.cache.RangeEntryCacheImpl;
26+
import org.apache.pulsar.common.allocator.ByteBufAllocatorStats;
27+
import org.apache.pulsar.common.allocator.PulsarByteBufAllocator;
2728
import org.apache.pulsar.opentelemetry.Constants;
2829
import org.apache.pulsar.opentelemetry.OpenTelemetryAttributes.CacheEntryStatus;
2930
import org.apache.pulsar.opentelemetry.OpenTelemetryAttributes.CacheOperationStatus;
@@ -160,7 +161,8 @@ private void recordMetrics(ManagedLedgerFactoryImpl factory) {
160161
cacheOperationCounter.record(stats.getCacheMissesTotal(), CacheOperationStatus.MISS.attributes);
161162
cacheOperationBytesCounter.record(stats.getCacheMissesBytesTotal(), CacheOperationStatus.MISS.attributes);
162163

163-
var allocatorStats = new PooledByteBufAllocatorStats(RangeEntryCacheImpl.ALLOCATOR);
164+
var allocatorStats = new ByteBufAllocatorStats(
165+
PulsarByteBufAllocator.getAllocatorMetric(ML_CACHE_ALLOCATOR_NAME));
164166
cachePoolActiveAllocationCounter.record(allocatorStats.activeAllocationsSmall, PoolArenaType.SMALL.attributes);
165167
cachePoolActiveAllocationCounter.record(allocatorStats.activeAllocationsNormal,
166168
PoolArenaType.NORMAL.attributes);

managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/ManagedCursorImpl.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -960,12 +960,20 @@ public void asyncReadEntriesWithSkip(int numberOfEntriesToRead, long maxSizeByte
960960
}
961961

962962
int numOfEntriesToRead = applyMaxSizeCap(numberOfEntriesToRead, maxSizeBytes);
963+
readEntriesWithSkip(numOfEntriesToRead, maxSizeBytes, callback, ctx, maxPosition, skipCondition);
964+
}
963965

966+
/**
967+
* Reads {@code numOfEntriesToRead} entries, a count that the caller already capped with {@code maxSizeBytes}, the
968+
* size limit the read carries along to bound its storage requests.
969+
*/
970+
private void readEntriesWithSkip(int numOfEntriesToRead, long maxSizeBytes, ReadEntriesCallback callback,
971+
Object ctx, Position maxPosition, Predicate<Position> skipCondition) {
964972
PENDING_READ_OPS_UPDATER.incrementAndGet(this);
965973
// Skip deleted entries.
966974
skipCondition = skipCondition == null ? this::isMessageDeleted : skipCondition.or(this::isMessageDeleted);
967-
OpReadEntry op =
968-
OpReadEntry.create(this, readPosition, numOfEntriesToRead, callback, ctx, maxPosition, skipCondition, true);
975+
OpReadEntry op = OpReadEntry.create(this, readPosition, numOfEntriesToRead, maxSizeBytes, callback, ctx,
976+
maxPosition, skipCondition, true);
969977
ledger.asyncReadEntries(op);
970978
}
971979

@@ -1117,12 +1125,11 @@ public void asyncReadEntriesWithSkipOrWait(int maxEntries, long maxSizeBytes, Re
11171125
if (hasMoreEntries() && maxPosition.compareTo(readPosition) >= 0) {
11181126
// If we have available entries, we can read them immediately
11191127
log.debug("Read entries immediately");
1120-
asyncReadEntriesWithSkip(numberOfEntriesToRead, NO_MAX_SIZE_LIMIT, callback, ctx,
1121-
maxPosition, skipCondition);
1128+
readEntriesWithSkip(numberOfEntriesToRead, maxSizeBytes, callback, ctx, maxPosition, skipCondition);
11221129
} else {
11231130
// Skip deleted entries.
11241131
skipCondition = skipCondition == null ? this::isMessageDeleted : skipCondition.or(this::isMessageDeleted);
1125-
OpReadEntry op = OpReadEntry.create(this, readPosition, numberOfEntriesToRead, callback,
1132+
OpReadEntry op = OpReadEntry.create(this, readPosition, numberOfEntriesToRead, maxSizeBytes, callback,
11261133
ctx, maxPosition, skipCondition, true);
11271134
int opReadId = op.id;
11281135
if (!WAITING_READ_OP_UPDATER.compareAndSet(this, null, op)) {

managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/ManagedLedgerImpl.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
import org.apache.bookkeeper.client.BKException.Code;
7777
import org.apache.bookkeeper.client.BookKeeper;
7878
import org.apache.bookkeeper.client.BookKeeper.DigestType;
79+
import org.apache.bookkeeper.client.BookKeeperClientConfigAccessor;
7980
import org.apache.bookkeeper.client.LedgerHandle;
8081
import org.apache.bookkeeper.client.api.LedgerEntry;
8182
import org.apache.bookkeeper.client.api.LedgerMetadata;
@@ -161,6 +162,8 @@ public class ManagedLedgerImpl implements ManagedLedger, CreateCallback {
161162
private static final Logger slog = Logger.get(ManagedLedgerImpl.class);
162163

163164
protected final BookKeeper bookKeeper;
165+
/** Whether the BookKeeper client can batch read: the v2 wire protocol with batch reads enabled. */
166+
private final boolean batchReadSupported;
164167
protected final String name;
165168
protected final Logger log;
166169

@@ -386,6 +389,7 @@ public ManagedLedgerImpl(ManagedLedgerFactoryImpl factory, BookKeeper bookKeeper
386389
}
387390
this.factory = factory;
388391
this.bookKeeper = bookKeeper;
392+
this.batchReadSupported = BookKeeperClientConfigAccessor.supportsBatchRead(bookKeeper);
389393
this.config = config;
390394
this.store = store;
391395
this.name = name;
@@ -1934,6 +1938,14 @@ synchronized void addEntryFailedDueToConcurrentlyModified(final LedgerHandle cur
19341938
+ " position when the ledger was concurrently modified"
19351939
+ " (the ledger may be closed by auto-replication)");
19361940
ledgerClosed(currentLedger, lh.getLastAddConfirmed());
1941+
// Close the abandoned write handle, or it leaks with its periodic explicit-LAC flush task.
1942+
currentLedger.asyncClose((closeRc, closedLedger, closeCtx) -> {
1943+
if (closeRc != Code.OK) {
1944+
log.debug().attr("ledgerId", currentLedger.getId())
1945+
.attr("status", BKException.getMessage(closeRc))
1946+
.log("Error when closing ledger after it was concurrently modified");
1947+
}
1948+
}, null);
19371949
} else {
19381950
log.error().attr("ledgerId", currentLedger.getId())
19391951
.attr("lastAddConfirmed", currentLedger.getLastAddConfirmed())
@@ -2492,12 +2504,14 @@ protected void asyncReadEntry(ReadHandle ledger, long firstEntry, long lastEntry
24922504
if (config.getReadEntryTimeoutSeconds() > 0) {
24932505
ReadEntryCallbackWrapper readCallback = ReadEntryCallbackWrapper.create(this, ledger.getId(), firstEntry,
24942506
opReadEntry, ctx, timeoutAtNanos(config.getReadEntryTimeoutSeconds()));
2495-
entryCache.asyncReadEntry(ledger, firstEntry, lastEntry, expectedReadCount, readCallback, ctx);
2507+
entryCache.asyncReadEntry(ledger, firstEntry, lastEntry, opReadEntry.maxSizeBytes, expectedReadCount,
2508+
readCallback, ctx);
24962509
if (readCallback.registerTimeout()) {
24972510
factory.getReadEntryTimeoutTracker().add(readCallback);
24982511
}
24992512
} else {
2500-
entryCache.asyncReadEntry(ledger, firstEntry, lastEntry, expectedReadCount, opReadEntry, ctx);
2513+
entryCache.asyncReadEntry(ledger, firstEntry, lastEntry, opReadEntry.maxSizeBytes, expectedReadCount,
2514+
opReadEntry, ctx);
25012515
}
25022516
}
25032517

@@ -4562,6 +4576,14 @@ public ManagedLedgerConfig getConfig() {
45624576
return config;
45634577
}
45644578

4579+
/**
4580+
* Whether storage reads use the BookKeeper batch read API: it must be enabled in the config and supported by
4581+
* the BookKeeper client (v2 wire protocol with batch reads enabled in its configuration).
4582+
*/
4583+
public boolean isBatchReadEnabled() {
4584+
return batchReadSupported && config.isBatchReadEnabled();
4585+
}
4586+
45654587
@Override
45664588
public void setConfig(ManagedLedgerConfig config) {
45674589
this.config = config;

0 commit comments

Comments
 (0)