Skip to content

[Bug][broker] topic compaction is failed after compactedLedger 3 quorum all being recover in bookie autoRecovery #21551

Description

@TakaHiR07

Search before asking

  • I searched in the issues and found nothing similar.

Version

branch-2.9 (guess master branch also have this problem)

Minimal reproduce step

reproduce step:

  1. __change_event policy topic, with compactedLedger 1705537, which has 3 quorum on bookie-1,bookie-2,bookie-3
  2. bookie-1,2,3 are shutdown one by one, trigger bookie autoRecovery
  3. compactedLedger 1705537 quorum become bookie-4,bookie-5,bookie-6
  4. use pulsar-admin api to setMessageTTL, trigger write message to __change_event topic
  5. broker log have many errors, show that compaction trigger in BrokerService#checkCompaction() is always failed

The error log is as following:

14:49:15.197 [compaction-76-1] INFO  org.apache.pulsar.client.impl.ConsumerImpl - [persistent://public/default/__change_events-partition-2][__compaction] Get topic last message Id
14:49:15.197 [broker-topic-workers-OrderedExecutor-1-0] ERROR org.apache.bookkeeper.proto.PerChannelBookieClient - Could not connect to bookie: null/bookie-1, current state CONNECTING : 
org.apache.bookkeeper.proto.BookieAddressResolver$BookieIdNotResolvedException: Cannot resolve bookieId bookie-1, bookie does not exist or it is not running
        at org.apache.bookkeeper.client.DefaultBookieAddressResolver.resolve(DefaultBookieAddressResolver.java:67) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.proto.PerChannelBookieClient.connect(PerChannelBookieClient.java:531) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.proto.PerChannelBookieClient.connectIfNeededAndDoOp(PerChannelBookieClient.java:657) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.proto.DefaultPerChannelBookieClientPool.obtain(DefaultPerChannelBookieClientPool.java:121) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.proto.DefaultPerChannelBookieClientPool.obtain(DefaultPerChannelBookieClientPool.java:116) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.proto.BookieClientImpl.readEntry(BookieClientImpl.java:509) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.proto.BookieClientImpl.readEntry(BookieClientImpl.java:495) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.proto.BookieClientImpl.readEntry(BookieClientImpl.java:489) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.client.PendingReadOp.sendReadTo(PendingReadOp.java:576) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.client.PendingReadOp$SequenceReadRequest.sendNextRead(PendingReadOp.java:405) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.client.PendingReadOp$SequenceReadRequest.read(PendingReadOp.java:386) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.client.PendingReadOp.initiate(PendingReadOp.java:530) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.client.PendingReadOp.safeRun(PendingReadOp.java:540) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.common.util.SafeRunnable.run(SafeRunnable.java:36) ~[org.apache.bookkeeper-bookkeeper-common-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.client.LedgerHandle.readEntriesInternalAsync(LedgerHandle.java:896) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.client.LedgerHandle.asyncReadEntriesInternal(LedgerHandle.java:800) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.client.LedgerHandle.asyncReadEntries(LedgerHandle.java:694) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.pulsar.compaction.CompactedTopicImpl.readOneMessageId(CompactedTopicImpl.java:224) ~[org.apache.pulsar-pulsar-broker-2.9.5.4.jar:2.9.5.4]
        at org.apache.pulsar.compaction.CompactedTopicImpl.lambda$createCache$7(CompactedTopicImpl.java:215) ~[org.apache.pulsar-pulsar-broker-2.9.5.4.jar:2.9.5.4]
        at com.github.benmanes.caffeine.cache.LocalAsyncCache.lambda$get$2(LocalAsyncCache.java:92) ~[com.github.ben-manes.caffeine-caffeine-2.9.1.jar:?]
        at com.github.benmanes.caffeine.cache.BoundedLocalCache.lambda$doComputeIfAbsent$14(BoundedLocalCache.java:2405) ~[com.github.ben-manes.caffeine-caffeine-2.9.1.jar:?]
        at java.util.concurrent.ConcurrentHashMap.compute(ConcurrentHashMap.java:1908) ~[?:?]
        at com.github.benmanes.caffeine.cache.BoundedLocalCache.doComputeIfAbsent(BoundedLocalCache.java:2403) ~[com.github.ben-manes.caffeine-caffeine-2.9.1.jar:?]
        at com.github.benmanes.caffeine.cache.BoundedLocalCache.computeIfAbsent(BoundedLocalCache.java:2386) ~[com.github.ben-manes.caffeine-caffeine-2.9.1.jar:?]
        at com.github.benmanes.caffeine.cache.LocalAsyncCache.get(LocalAsyncCache.java:91) ~[com.github.ben-manes.caffeine-caffeine-2.9.1.jar:?]
        at com.github.benmanes.caffeine.cache.LocalAsyncCache.get(LocalAsyncCache.java:82) ~[com.github.ben-manes.caffeine-caffeine-2.9.1.jar:?]
        at com.github.benmanes.caffeine.cache.LocalAsyncLoadingCache.get(LocalAsyncLoadingCache.java:79) ~[com.github.ben-manes.caffeine-caffeine-2.9.1.jar:?]
        at org.apache.pulsar.compaction.CompactedTopicImpl.findStartPointLoop(CompactedTopicImpl.java:189) ~[org.apache.pulsar-pulsar-broker-2.9.5.4.jar:2.9.5.4]
        at org.apache.pulsar.compaction.CompactedTopicImpl.findStartPoint(CompactedTopicImpl.java:179) ~[org.apache.pulsar-pulsar-broker-2.9.5.4.jar:2.9.5.4]
        at org.apache.pulsar.compaction.CompactedTopicImpl.lambda$asyncReadEntriesOrWait$3(CompactedTopicImpl.java:106) ~[org.apache.pulsar-pulsar-broker-2.9.5.4.jar:2.9.5.4]
        at java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1106) ~[?:?]
        at java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2235) ~[?:?]
        at org.apache.pulsar.compaction.CompactedTopicImpl.asyncReadEntriesOrWait(CompactedTopicImpl.java:105) ~[org.apache.pulsar-pulsar-broker-2.9.5.4.jar:2.9.5.4]
        at org.apache.pulsar.broker.service.persistent.PersistentDispatcherSingleActiveConsumer.readMoreEntries(PersistentDispatcherSingleActiveConsumer.java:343) ~[org.apache.pulsar-pulsar-broker-2.9.5.4.jar:2.9.5.4]
        at org.apache.pulsar.broker.service.persistent.PersistentDispatcherSingleActiveConsumer.internalConsumerFlow(PersistentDispatcherSingleActiveConsumer.java:280) ~[org.apache.pulsar-pulsar-broker-2.9.5.4.jar:2.9.5.4]
        at org.apache.pulsar.broker.service.persistent.PersistentDispatcherSingleActiveConsumer.lambda$consumerFlow$5(PersistentDispatcherSingleActiveConsumer.java:256) ~[org.apache.pulsar-pulsar-broker-2.9.5.4.jar:2.9.5.4]
        at org.apache.bookkeeper.mledger.util.SafeRun$1.safeRun(SafeRun.java:32) ~[org.apache.pulsar-managed-ledger-2.9.5.4.jar:2.9.5.4]
        at org.apache.bookkeeper.common.util.SafeRunnable.run(SafeRunnable.java:36) ~[org.apache.bookkeeper-bookkeeper-common-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.common.util.OrderedExecutor$TimedRunnable.run(OrderedExecutor.java:204) ~[org.apache.bookkeeper-bookkeeper-common-4.14.1.jar:4.14.1]
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[?:?]
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[?:?]
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[io.netty-netty-common-4.1.66.Final.jar:4.1.66.Final]
        at java.lang.Thread.run(Thread.java:834) ~[?:?]
Caused by: org.apache.bookkeeper.client.BKException$BKBookieHandleNotAvailableException: Bookie handle is not available
        at org.apache.bookkeeper.discover.ZKRegistrationClient.getBookieServiceInfo(ZKRegistrationClient.java:248) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        at org.apache.bookkeeper.client.DefaultBookieAddressResolver.resolve(DefaultBookieAddressResolver.java:47) ~[org.apache.bookkeeper-bookkeeper-server-4.14.1.jar:4.14.1]
        ... 42 more
14:49:15.198 [BookKeeperClientWorker-OrderedExecutor-1-0] ERROR org.apache.bookkeeper.client.PendingReadOp - Read of ledger entry failed: L1705537 E0-E0, Sent to [bookie-1, bookie-2, bookie-3], Heard from [] : bitset = {}, Error = 'Bookie handle is not available'. First unread entry is (-1, rc = null)
14:49:15.199 [broker-topic-workers-OrderedExecutor-1-0] ERROR org.apache.pulsar.broker.service.persistent.PersistentDispatcherSingleActiveConsumer - [persistent://public/default/__change_events-partition-2 / __compaction-Consumer{subscription=CompactorSubscription{topic=persistent://public/default/__change_events-partition-2, name=__compaction}, consumerId=1011, consumerName=1bdee, address=/xxx}] Error reading entries at 1705285:48 : java.util.concurrent.CompletionException: org.apache.bookkeeper.client.BKException$BKBookieHandleNotAvailableException: Bookie handle is not available - Retrying to read in 54.759 seconds
14:49:15.199 [pulsar-io-32-26] INFO  org.apache.pulsar.client.impl.ConsumerImpl - [persistent://public/default/__change_events-partition-2][__compaction] Successfully getLastMessageId 1705285:48
14:49:15.199 [pulsar-io-32-26] INFO  org.apache.pulsar.compaction.TwoPhaseCompactor - Commencing phase one of compaction for persistent://public/default/__change_events-partition-2, reading to 1705285:48:2

We found that the reason is reading compacted ledger 1705537 failed in the process of TwoPhaseCompaction.

When do TwoPhaseCompaction. The step is :

  1. judge topic has message by reader.hasMessageAvailableAsync()
  2. do phaseOne compact. During the process it would getLastMessageId and trigger phaseOneLoop()
  3. do phaseTwo compact. create new compactedLedger and compact all keys to compactedLedger. trigger phaseTwoSeekThenLoop()

In the phaseOneLoop(), it would try to read message, trigger CompactedTopicImpl#asyncReadEntriesOrWait(). And then it would use compactedTopicContext to findStartPoint. compactedTopicContext is a local variable, containing ReadOnlyLedgerHandle and AsyncLoadingCache. compactedTopicContext is updated in the previous compaction, and then it would not change until the next compaction finish.

Previous compaction make compactedTopicContext, 1705537 LedgerHandle's metadata is [bookie-1, bookie-2, bookie-3]. However, after bookie shutdown and do bookie autoRecovery, the 3 quorum on 1705537 has been changed to [bookie-4, bookie-5, bookie-6]. But the LedgerHandle's metadata in broker is not change. So it still try to read message of 1705537 from bookie-1,2,3 and fail, causing the compaction process can not success.

The relevant code is:

public CompletableFuture<CompactedTopicContext> newCompactedLedger(Position p, long compactedLedgerId) {
synchronized (this) {
CompletableFuture<CompactedTopicContext> previousContext = compactedTopicContext;
compactedTopicContext = openCompactedLedger(bk, compactedLedgerId);
compactionHorizon = (PositionImpl) p;
// delete the ledger from the old context once the new one is open
return compactedTopicContext.thenCompose(
__ -> previousContext != null ? previousContext : CompletableFuture.completedFuture(null));
}
}
@Override
public CompletableFuture<Void> deleteCompactedLedger(long compactedLedgerId) {
return tryDeleteCompactedLedger(bk, compactedLedgerId);
}
@Override
@Deprecated
public void asyncReadEntriesOrWait(ManagedCursor cursor,
int maxEntries,
long bytesToRead,
PositionImpl maxReadPosition,
boolean isFirstRead,
ReadEntriesCallback callback, Consumer consumer) {
PositionImpl cursorPosition;
if (isFirstRead && MessageId.earliest.equals(consumer.getStartMessageId())){
cursorPosition = PositionImpl.EARLIEST;
} else {
cursorPosition = (PositionImpl) cursor.getReadPosition();
}
// TODO: redeliver epoch link https://github.com/apache/pulsar/issues/13690
ReadEntriesCtx readEntriesCtx = ReadEntriesCtx.create(consumer, DEFAULT_CONSUMER_EPOCH);
final PositionImpl currentCompactionHorizon = compactionHorizon;
if (currentCompactionHorizon == null
|| currentCompactionHorizon.compareTo(cursorPosition) < 0) {
cursor.asyncReadEntriesOrWait(maxEntries, bytesToRead, callback, readEntriesCtx, maxReadPosition);
} else {
ManagedCursorImpl managedCursor = (ManagedCursorImpl) cursor;
int numberOfEntriesToRead = managedCursor.applyMaxSizeCap(maxEntries, bytesToRead);
compactedTopicContext.thenCompose(
(context) -> findStartPoint(cursorPosition, context.ledger.getLastAddConfirmed(), context.cache)
.thenCompose((startPoint) -> {
// do not need to read the compaction ledger if it is empty.
// the cursor just needs to be set to the compaction horizon
if (startPoint == COMPACT_LEDGER_EMPTY || startPoint == NEWER_THAN_COMPACTED) {
cursor.seek(currentCompactionHorizon.getNext());
callback.readEntriesComplete(Collections.emptyList(), readEntriesCtx);
return CompletableFuture.completedFuture(null);
} else {
long endPoint = Math.min(context.ledger.getLastAddConfirmed(),
startPoint + (numberOfEntriesToRead - 1));
return readEntries(context.ledger, startPoint, endPoint)
.thenAccept((entries) -> {
long entriesSize = 0;
for (Entry entry : entries) {
entriesSize += entry.getLength();
}
managedCursor.updateReadStats(entries.size(), entriesSize);
Entry lastEntry = entries.get(entries.size() - 1);
// The compaction task depends on the last snapshot and the incremental
// entries to build the new snapshot. So for the compaction cursor, we
// need to force seek the read position to ensure the compactor can read
// the complete last snapshot because of the compactor will read the data
// before the compaction cursor mark delete position
cursor.seek(lastEntry.getPosition().getNext(), true);
callback.readEntriesComplete(entries, readEntriesCtx);
});
}
}))
.exceptionally((exception) -> {
if (exception.getCause() instanceof NoSuchElementException) {
cursor.seek(currentCompactionHorizon.getNext());
callback.readEntriesComplete(Collections.emptyList(), readEntriesCtx);
} else {
callback.readEntriesFailed(new ManagedLedgerException(exception), readEntriesCtx);
}
return null;
});
}
}
static CompletableFuture<Long> findStartPoint(PositionImpl p,
long lastEntryId,
AsyncLoadingCache<Long, MessageIdData> cache) {
CompletableFuture<Long> promise = new CompletableFuture<>();
// if lastEntryId is less than zero it means there are no entries in the compact ledger
if (lastEntryId < 0) {
promise.complete(COMPACT_LEDGER_EMPTY);
} else {
findStartPointLoop(p, 0, lastEntryId, promise, cache);
}
return promise;
}
@VisibleForTesting
static void findStartPointLoop(PositionImpl p, long start, long end,
CompletableFuture<Long> promise,
AsyncLoadingCache<Long, MessageIdData> cache) {
long midpoint = start + ((end - start) / 2);
CompletableFuture<MessageIdData> startEntry = cache.get(start);
CompletableFuture<MessageIdData> middleEntry = cache.get(midpoint);
CompletableFuture<MessageIdData> endEntry = cache.get(end);
CompletableFuture.allOf(startEntry, middleEntry, endEntry).thenRun(
() -> {
if (comparePositionAndMessageId(p, startEntry.join()) <= 0) {
promise.complete(start);
} else if (comparePositionAndMessageId(p, middleEntry.join()) <= 0) {
findStartPointLoop(p, start + 1, midpoint, promise, cache);
} else if (comparePositionAndMessageId(p, endEntry.join()) <= 0) {
findStartPointLoop(p, midpoint + 1, end, promise, cache);
} else {
promise.complete(NEWER_THAN_COMPACTED);
}
}).exceptionally((exception) -> {
promise.completeExceptionally(exception);
return null;
});
}
static AsyncLoadingCache<Long, MessageIdData> createCache(LedgerHandle lh,
long maxSize) {
return Caffeine.newBuilder()
.maximumSize(maxSize)
.buildAsync((entryId, executor) -> readOneMessageId(lh, entryId));
}

@Override
protected CompletableFuture<Long> doCompaction(RawReader reader, BookKeeper bk) {
return reader.hasMessageAvailableAsync()
.thenCompose(available -> {
if (available) {
return phaseOne(reader).thenCompose(
(r) -> phaseTwo(reader, r.from, r.to, r.lastReadId, r.latestForKey, bk));
} else {
log.info("Skip compaction of the empty topic {}", reader.getTopic());
return CompletableFuture.completedFuture(-1L);
}
});
}
private CompletableFuture<PhaseOneResult> phaseOne(RawReader reader) {
Map<String, MessageId> latestForKey = new HashMap<>();
CompletableFuture<PhaseOneResult> loopPromise = new CompletableFuture<>();
reader.getLastMessageIdAsync()
.thenAccept(lastMessageId -> {
log.info("Commencing phase one of compaction for {}, reading to {}",
reader.getTopic(), lastMessageId);
// Each entry is processed as a whole, discard the batchIndex part deliberately.
MessageIdImpl lastImpl = (MessageIdImpl) lastMessageId;
MessageIdImpl lastEntryMessageId = new MessageIdImpl(lastImpl.getLedgerId(), lastImpl.getEntryId(),
lastImpl.getPartitionIndex());
phaseOneLoop(reader, Optional.empty(), Optional.empty(), lastEntryMessageId, latestForKey,
loopPromise);
}).exceptionally(ex -> {
loopPromise.completeExceptionally(ex);
return null;
});
return loopPromise;
}
private void phaseOneLoop(RawReader reader,
Optional<MessageId> firstMessageId,
Optional<MessageId> toMessageId,
MessageId lastMessageId,
Map<String, MessageId> latestForKey,
CompletableFuture<PhaseOneResult> loopPromise) {
if (loopPromise.isDone()) {
return;
}
CompletableFuture<RawMessage> future = reader.readNextAsync();
FutureUtil.addTimeoutHandling(future,
phaseOneLoopReadTimeout, scheduler,
() -> FutureUtil.createTimeoutException("Timeout", getClass(), "phaseOneLoop(...)"));
future.thenAcceptAsync(m -> {
try (m) {
MessageId id = m.getMessageId();
boolean deletedMessage = false;
boolean replaceMessage = false;
mxBean.addCompactionReadOp(reader.getTopic(), m.getHeadersAndPayload().readableBytes());
MessageMetadata metadata = Commands.parseMessageMetadata(m.getHeadersAndPayload());
if (RawBatchConverter.isReadableBatch(metadata)) {
try {
int numMessagesInBatch = metadata.getNumMessagesInBatch();
int deleteCnt = 0;
for (ImmutableTriple<MessageId, String, Integer> e : extractIdsAndKeysAndSizeFromBatch(m)) {
if (e != null) {
if (e.getRight() > 0) {
MessageId old = latestForKey.put(e.getMiddle(), e.getLeft());
if (old != null) {
mxBean.addCompactionRemovedEvent(reader.getTopic());
}
} else {
latestForKey.remove(e.getMiddle());
deleteCnt++;
mxBean.addCompactionRemovedEvent(reader.getTopic());
}
}
}
if (deleteCnt == numMessagesInBatch) {
deletedMessage = true;
}
} catch (IOException ioe) {
log.info("Error decoding batch for message {}. Whole batch will be included in output",
id, ioe);
}
} else {
Pair<String, Integer> keyAndSize = extractKeyAndSize(m);
if (keyAndSize != null) {
if (keyAndSize.getRight() > 0) {
MessageId old = latestForKey.put(keyAndSize.getLeft(), id);
replaceMessage = old != null;
} else {
deletedMessage = true;
latestForKey.remove(keyAndSize.getLeft());
}
}
if (replaceMessage || deletedMessage) {
mxBean.addCompactionRemovedEvent(reader.getTopic());
}
}
MessageId first = firstMessageId.orElse(deletedMessage ? null : id);
MessageId to = deletedMessage ? toMessageId.orElse(null) : id;
if (id.compareTo(lastMessageId) == 0) {
loopPromise.complete(new PhaseOneResult(first == null ? id : first, to == null ? id : to,
lastMessageId, latestForKey));
} else {
phaseOneLoop(reader,
Optional.ofNullable(first),
Optional.ofNullable(to),
lastMessageId,
latestForKey, loopPromise);
}
}
}, scheduler).exceptionally(ex -> {
loopPromise.completeExceptionally(ex);
return null;
});
}
private CompletableFuture<Long> phaseTwo(RawReader reader, MessageId from, MessageId to, MessageId lastReadId,
Map<String, MessageId> latestForKey, BookKeeper bk) {
Map<String, byte[]> metadata =
LedgerMetadataUtils.buildMetadataForCompactedLedger(reader.getTopic(), to.toByteArray());
return createLedger(bk, metadata).thenCompose((ledger) -> {
log.info("Commencing phase two of compaction for {}, from {} to {}, compacting {} keys to ledger {}",
reader.getTopic(), from, to, latestForKey.size(), ledger.getId());
return phaseTwoSeekThenLoop(reader, from, to, lastReadId, latestForKey, bk, ledger);
});
}

We temporarily fix the problem by restart all the broker. Because after broker restart, the previous compactedTopicContext is not exist, compaction can succeed.

But this is not a user-friendly way since each time bookie shutdown, we may need to restart broker. We‘d better fix this issue.

After diving to the code, we found that there are two way to open ReadOnlyLedgerHandle in bookie, asyncOpenLedgerNoRecovery and asyncOpenLedger.

  • asyncOpenLedger would try to open ledger and if the ledger‘s metadata‘s state is OPEN, it may try to recover. But it would not register the metadata zk node update listener.
  • Only in asyncOpenLedgerNoRecovery would watch the zk metadata and update the local metadata.

We can replace asyncOpenLedger to asyncOpenLedgerNoRecovery. I guess the compactedLedger would not in open state, and it need to update metadata.

What did you expect to see?

compaction can succeed after compactedLedger 3 quorum is autoRecover

What did you see instead?

compaction keep failed after compactedLedger 3 quorum is autoRecover

Anything else?

No response

Are you willing to submit a PR?

  • I'm willing to submit a PR!

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/bugThe PR fixed a bug or issue reported a bug

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions