You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bifromq.apache.org by gu...@apache.org on 2025/09/15 14:59:41 UTC
(bifromq) branch main updated: Various improvements/fixes on long-running stability (#176)
This is an automated email from the ASF dual-hosted git repository.
gujiaweijoe pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/bifromq.git
The following commit(s) were added to refs/heads/main by this push:
new 18e33d6d Various improvements/fixes on long-running stability (#176)
18e33d6d is described below
commit 18e33d6d565de80cd76c9c1ca0c4d5770b345181
Author: Yonny(Yu) Hao <po...@gmail.com>
AuthorDate: Mon Sep 15 22:59:37 2025 +0800
Various improvements/fixes on long-running stability (#176)
Core Stability & Bug Fixes
Fix stability issues and race conditions
- Prevent NPE when landscape lacks local descriptor
- Fix config change failure by forcing WAL compaction on FSM apply
- Send EnsureRequest only from leader during config changes
- Quit zombie replicas only when not in current config
- Fix race causing pipeline retargeting to stall
- Correctly handle duplicate matchinfo in inbox ingestion
- Fix RedundantRangeRemovalBalancer mistakenly removing valid ranges
- Fix ReplicaCntBalancer unbalanced state in edge cases
Balancing & Resource Management
Improve balancer stability and efficiency
- Enhance RangeSplitBalancer and ReplicaCntBalancer for edge cases
- Support partial load rules in BalancerController
- Reduce HostMemberList and AGENT_HOST_MAP sync overhead
- Exclude terminated ranges from effective routing
- Optimize balancer logging output
- Improve built-in balancer efficiency
CRDT & Anti-Entropy
CRDT and anti-entropy improvements
- Correct log context and support MDCLogger with lambdas
- Expose refute signal to speed up CRDT convergence
- Improve housekeeping logic in CRDT-based metadata service
- Correctly meter delta send rate and throughput
- Improve stale member cleanup logic
- Anti-entropy refinements:
* Reset resendCount on ACK to avoid spurious resets
* Continue syncing after ACK to drain deltas
* Leverage late/unmatched ACKs when possible
Performance & Reliability
Optimize performance and backpressure handling
- Reduce memory overhead in argument formatter
- Improve backpressure when downstream stalls
- Optimize bootstrap and config change workflow
Miscellaneous
Chores and maintenance
- Remove deprecated proto fields
- Correct code format in Settings file
- Enable manually triggered Coverity build
---
.github/workflows/build-cov.yaml | 1 +
base-cluster/pom.xml | 4 +
.../org/apache/bifromq/basecluster/AgentHost.java | 43 +-
.../org/apache/bifromq/basecluster/IAgentHost.java | 20 +-
.../basecluster/memberlist/HostMemberList.java | 108 ++--
.../basecluster/memberlist/IHostMemberList.java | 25 +-
.../basecluster/memberlist/agent/Agent.java | 67 ++-
.../basecluster/memberlist/agent/AgentMember.java | 43 +-
.../basecluster/memberlist/agent/IAgent.java | 29 +-
.../basecluster/memberlist/agent/IAgentMember.java | 51 +-
.../proto/basecluster/membership/HostMember.proto | 2 +-
.../apache/bifromq/basecluster/AgentHostsTest.java | 72 ++-
.../bifromq/basecluster/AgentTestCluster.java | 51 +-
.../bifromq/basecluster/AgentTestTemplate.java | 14 +-
.../basecluster/memberlist/HostMemberListTest.java | 59 +--
base-crdt/base-crdt-service/pom.xml | 4 +
.../bifromq/basecrdt/service/CRDTCluster.java | 2 +-
.../bifromq/basecrdt/service/CRDTService.java | 5 +
.../bifromq/basecrdt/service/ICRDTService.java | 17 +-
.../basecrdt/core/internal/AWORSetInflater.java | 9 +-
.../basecrdt/core/internal/CCounterInflater.java | 9 +-
.../basecrdt/core/internal/CausalCRDTInflater.java | 6 +-
.../core/internal/CausalCRDTInflaterFactory.java | 25 +-
.../basecrdt/core/internal/DWFlagInflater.java | 9 +-
.../basecrdt/core/internal/EWFlagInflater.java | 8 +-
.../core/internal/InMemReplicaStateLattice.java | 7 +-
.../basecrdt/core/internal/MVRegInflater.java | 11 +-
.../basecrdt/core/internal/ORMapInflater.java | 11 +-
.../basecrdt/core/internal/RWORSetInflater.java | 11 +-
.../apache/bifromq/basecrdt/store/AntiEntropy.java | 69 ++-
.../bifromq/basecrdt/store/AntiEntropyManager.java | 14 +-
.../apache/bifromq/basecrdt/store/CRDTStore.java | 1 +
.../apache/bifromq/basecrdt/util/Formatter.java | 50 +-
.../core/benchmark/CRDTBenchmarkTemplate.java | 6 +-
.../basecrdt/core/internal/AWORSetTest.java | 16 +-
.../basecrdt/core/internal/CCounterTest.java | 22 +-
.../bifromq/basecrdt/core/internal/CRDTTest.java | 6 +-
.../bifromq/basecrdt/core/internal/DWFlagTest.java | 14 +-
.../bifromq/basecrdt/core/internal/EWFlagTest.java | 14 +-
.../internal/InMemReplicaStateLatticeTest.java | 25 +-
.../bifromq/basecrdt/core/internal/MVRegTest.java | 18 +-
.../bifromq/basecrdt/core/internal/ORMapTest.java | 32 +-
.../basecrdt/core/internal/RWORSetTest.java | 14 +-
.../basecrdt/store/AntiEntropyResilienceTest.java | 204 ++++++++
base-kv/base-kv-meta-service/pom.xml | 8 +
.../basekv/metaservice/BaseKVLandscapeCRDT.java | 49 +-
.../metaservice/BaseKVLandscapeObserver.java | 5 +
.../metaservice/BaseKVLandscapeReporter.java | 37 +-
.../metaservice/BaseKVStoreBalancerStatesCRDT.java | 52 +-
.../BaseKVStoreBalancerStatesObserver.java | 5 +
.../BaseKVStoreBalancerStatesProposalCRDT.java | 14 +-
.../BaseKVStoreBalancerStatesProposer.java | 5 +
.../BaseKVStoreBalancerStatesReporter.java | 36 +-
.../basekv/metaservice/IBaseKVLandscapeCRDT.java | 13 +
.../metaservice/IBaseKVLandscapeReporter.java | 8 +
.../IBaseKVStoreBalancerStatesCRDT.java | 4 +
.../IBaseKVStoreBalancerStatesProposalCRDT.java | 1 +
.../IBaseKVStoreBalancerStatesReporter.java | 8 +
.../metaservice/BaseKVLandscapeReportTest.java | 5 +-
.../bifromq/basekv/raft/RaftNodeStateFollower.java | 26 +-
.../basekv/raft/exception/SnapshotException.java | 36 +-
.../basekv/balance/KVStoreBalanceController.java | 82 ++-
.../RedundantRangeRemovalBalancerFactory.java | 10 +-
.../balance/impl/RangeBootstrapBalancer.java | 36 +-
.../basekv/balance/impl/RangeSplitBalancer.java | 16 +
.../impl/RedundantRangeRemovalBalancer.java | 153 ++++--
.../basekv/balance/impl/ReplicaCntBalancer.java | 440 +++++++++-------
.../impl/UnreachableReplicaRemovalBalancer.java | 23 +-
.../balance/KVStoreBalanceControllerTest.java | 14 +
.../balance/impl/RangeBootstrapBalancerTest.java | 61 ++-
.../balance/impl/RangeLeaderBalancerTest.java | 14 +-
.../balance/impl/RangeSplitBalancerTest.java | 172 +++++++
.../impl/RedundantRangeRemovalBalancerTest.java | 80 ++-
.../balance/impl/ReplicaCntBalancerTest.java | 562 +++++++++++++++++++--
.../UnreachableReplicaRemovalBalancerTest.java | 53 +-
.../bifromq/basekv/balance/StoreBalancer.java | 3 +-
.../basekv/balance/command/RecoveryCommand.java | 4 +-
.../bifromq/basekv/balance/util/CommandUtil.java | 30 +-
.../basekv/balance/util/CommandUtilTest.java | 30 +-
.../bifromq/basekv/server/BaseKVStoreService.java | 4 +-
.../bifromq/basekv/store/range/KVRangeFSM.java | 257 +++++-----
.../basekv/store/wal/KVRangeWALSubscription.java | 3 +-
.../bifromq/basekv/utils/DescriptorUtil.java | 31 +-
.../bifromq/basekv/utils/DescriptorUtilTest.java | 274 +++++++++-
.../java/org/apache/bifromq/logger/MDCLogger.java | 10 +-
.../apache/bifromq/baserpc/client/BiDiStream.java | 6 +-
.../bifromq/baserpc/client/ClientChannel.java | 22 +-
.../bifromq/baserpc/client/ManagedBiDiStream.java | 71 ++-
.../loadbalancer/TrafficDirectiveLoadBalancer.java | 44 +-
.../baserpc/server/AbstractResponsePipeline.java | 11 +-
base-rpc/base-rpc-traffic-governor/pom.xml | 16 +
.../trafficgovernor/RPCServiceTrafficManager.java | 64 ++-
.../org/apache/bifromq/basescheduler/Batcher.java | 79 +--
.../org/apache/bifromq/basescheduler/EMALong.java | 49 +-
.../basescheduler/BatchCallSchedulerTest.java | 22 +-
.../apache/bifromq/basescheduler/EMALongTest.java | 8 +-
base-util/pom.xml | 4 +
.../apache/bifromq/base/util}/RendezvousHash.java | 6 +-
.../bifromq/base/util}/RendezvousHashTest.java | 2 +-
.../bifromq/dist/server/DistServiceTest.java | 3 +-
.../bifromq/dist/worker/DeliverExecutorGroup.java | 1 +
.../apache/bifromq/dist/worker/DistWorkerTest.java | 3 +-
.../main/proto/inboxservice/InboxStoreCoProc.proto | 11 +-
.../apache/bifromq/inbox/server/InboxWriter.java | 91 +++-
.../bifromq/inbox/server/InboxServiceTest.java | 23 +-
.../bifromq/inbox/server/InboxWriterTest.java | 19 +-
.../bifromq/inbox/store/InboxStoreCoProc.java | 44 +-
.../bifromq/inbox/store/InboxInsertTest.java | 232 ++++++++-
.../apache/bifromq/inbox/store/InboxStoreTest.java | 21 +-
.../bifromq/inbox/store/LoadExistingTest.java | 2 +-
.../bifromq/inbox/store/LoadSubStatsTest.java | 4 +-
.../apache/bifromq/mqtt/integration/MQTTTest.java | 9 +-
.../bifromq/plugin/settingprovider/Setting.java | 12 +-
.../bifromq/retain/store/LoadMetadataTest.java | 6 +-
.../bifromq/retain/store/RetainStoreTest.java | 43 +-
build/build-bifromq-starter/conf/log4j2.xml | 2 +-
116 files changed, 3576 insertions(+), 1256 deletions(-)
diff --git a/.github/workflows/build-cov.yaml b/.github/workflows/build-cov.yaml
index 5fa51550..bd0c4b8e 100644
--- a/.github/workflows/build-cov.yaml
+++ b/.github/workflows/build-cov.yaml
@@ -1,6 +1,7 @@
name: Cov-Build
on:
+ workflow_dispatch:
pull_request:
branches:
- 'main'
diff --git a/base-cluster/pom.xml b/base-cluster/pom.xml
index 34c97841..1bcc0808 100644
--- a/base-cluster/pom.xml
+++ b/base-cluster/pom.xml
@@ -33,6 +33,10 @@
<groupId>org.apache.bifromq</groupId>
<artifactId>base-env-provider</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.bifromq</groupId>
+ <artifactId>base-util</artifactId>
+ </dependency>
<dependency>
<groupId>org.apache.bifromq</groupId>
<artifactId>base-hlc</artifactId>
diff --git a/base-cluster/src/main/java/org/apache/bifromq/basecluster/AgentHost.java b/base-cluster/src/main/java/org/apache/bifromq/basecluster/AgentHost.java
index 1633b3cf..d7423027 100644
--- a/base-cluster/src/main/java/org/apache/bifromq/basecluster/AgentHost.java
+++ b/base-cluster/src/main/java/org/apache/bifromq/basecluster/AgentHost.java
@@ -14,14 +14,31 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecluster;
-import static org.apache.bifromq.basecluster.memberlist.CRDTUtil.AGENT_HOST_MAP_URI;
import static com.google.common.base.Preconditions.checkArgument;
+import static org.apache.bifromq.basecluster.memberlist.CRDTUtil.AGENT_HOST_MAP_URI;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.protobuf.ByteString;
+import io.micrometer.core.instrument.Metrics;
+import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
+import io.reactivex.rxjava3.core.Observable;
+import io.reactivex.rxjava3.core.Scheduler;
+import io.reactivex.rxjava3.disposables.CompositeDisposable;
+import io.reactivex.rxjava3.schedulers.Schedulers;
+import java.net.InetSocketAddress;
+import java.time.Duration;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicReference;
+import lombok.extern.slf4j.Slf4j;
import org.apache.bifromq.basecluster.agent.proto.AgentEndpoint;
import org.apache.bifromq.basecluster.fd.FailureDetector;
import org.apache.bifromq.basecluster.fd.IFailureDetector;
@@ -43,23 +60,6 @@ import org.apache.bifromq.basecluster.transport.ITransport;
import org.apache.bifromq.basecrdt.store.ICRDTStore;
import org.apache.bifromq.basecrdt.store.proto.CRDTStoreMessage;
import org.apache.bifromq.baseenv.EnvProvider;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
-import com.google.protobuf.ByteString;
-import io.micrometer.core.instrument.Metrics;
-import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
-import io.reactivex.rxjava3.core.Observable;
-import io.reactivex.rxjava3.core.Scheduler;
-import io.reactivex.rxjava3.disposables.CompositeDisposable;
-import io.reactivex.rxjava3.schedulers.Schedulers;
-import java.net.InetSocketAddress;
-import java.time.Duration;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.atomic.AtomicReference;
-import lombok.extern.slf4j.Slf4j;
@Slf4j
final class AgentHost implements IAgentHost {
@@ -173,6 +173,11 @@ final class AgentHost implements IAgentHost {
return memberList.landscape();
}
+ @Override
+ public Observable<Long> refuteSignal() {
+ return memberList.refuteSignal();
+ }
+
@Override
public void close() {
if (state.compareAndSet(State.STARTED, State.STOPPING)) {
diff --git a/base-cluster/src/main/java/org/apache/bifromq/basecluster/IAgentHost.java b/base-cluster/src/main/java/org/apache/bifromq/basecluster/IAgentHost.java
index e804f4e5..d5b26aa4 100644
--- a/base-cluster/src/main/java/org/apache/bifromq/basecluster/IAgentHost.java
+++ b/base-cluster/src/main/java/org/apache/bifromq/basecluster/IAgentHost.java
@@ -14,11 +14,16 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecluster;
+import io.reactivex.rxjava3.core.Observable;
+import java.net.InetSocketAddress;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import org.apache.bifromq.basecluster.memberlist.HostAddressResolver;
import org.apache.bifromq.basecluster.memberlist.IHostAddressResolver;
import org.apache.bifromq.basecluster.memberlist.agent.IAgent;
@@ -26,11 +31,6 @@ import org.apache.bifromq.basecluster.membership.proto.HostEndpoint;
import org.apache.bifromq.basecluster.transport.ITransport;
import org.apache.bifromq.basecluster.transport.TCPTransport;
import org.apache.bifromq.basecluster.transport.Transport;
-import io.reactivex.rxjava3.core.Observable;
-import java.net.InetSocketAddress;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
/**
* Agent host defines the interface for hosting agents and joining the cluster.
@@ -101,6 +101,14 @@ public interface IAgentHost extends AutoCloseable {
*/
Observable<Map<HostEndpoint, Set<String>>> landscape();
+ /**
+ * Emits a signal whenever the local host actively refutes a suspicion of being dead.
+ * Each emission carries the timestamp (in millis) when the refutation occurred.
+ *
+ * @return an observable stream of refutation timestamps
+ */
+ Observable<Long> refuteSignal();
+
/**
* Shutdown the agent host.
*/
diff --git a/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/HostMemberList.java b/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/HostMemberList.java
index df208694..25868a20 100644
--- a/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/HostMemberList.java
+++ b/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/HostMemberList.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecluster.memberlist;
@@ -25,28 +25,8 @@ import static org.apache.bifromq.basecluster.memberlist.CRDTUtil.iterate;
import static org.apache.bifromq.basecrdt.core.api.CausalCRDTType.mvreg;
import static org.apache.bifromq.basecrdt.store.ReplicaIdGenerator.generate;
-import org.apache.bifromq.basecluster.agent.proto.AgentEndpoint;
-import org.apache.bifromq.basecluster.memberlist.agent.Agent;
-import org.apache.bifromq.basecluster.memberlist.agent.AgentAddressProvider;
-import org.apache.bifromq.basecluster.memberlist.agent.AgentMessenger;
-import org.apache.bifromq.basecluster.memberlist.agent.IAgent;
-import org.apache.bifromq.basecluster.membership.proto.Doubt;
-import org.apache.bifromq.basecluster.membership.proto.Fail;
-import org.apache.bifromq.basecluster.membership.proto.HostEndpoint;
-import org.apache.bifromq.basecluster.membership.proto.HostMember;
-import org.apache.bifromq.basecluster.membership.proto.Join;
-import org.apache.bifromq.basecluster.membership.proto.Quit;
-import org.apache.bifromq.basecluster.messenger.IMessenger;
-import org.apache.bifromq.basecluster.proto.ClusterMessage;
-import org.apache.bifromq.basecrdt.core.api.IORMap;
-import org.apache.bifromq.basecrdt.core.api.MVRegOperation;
-import org.apache.bifromq.basecrdt.core.api.ORMapOperation;
-import org.apache.bifromq.basecrdt.proto.Replica;
-import org.apache.bifromq.basecrdt.store.ICRDTStore;
-import org.apache.bifromq.basehlc.HLC;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
import com.google.protobuf.AbstractMessageLite;
import com.google.protobuf.ByteString;
import io.micrometer.core.instrument.Gauge;
@@ -57,6 +37,7 @@ import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Scheduler;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
+import io.reactivex.rxjava3.subjects.PublishSubject;
import java.net.InetSocketAddress;
import java.util.HashSet;
import java.util.Iterator;
@@ -69,7 +50,30 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.base.util.RendezvousHash;
+import org.apache.bifromq.basecluster.agent.proto.AgentEndpoint;
+import org.apache.bifromq.basecluster.memberlist.agent.Agent;
+import org.apache.bifromq.basecluster.memberlist.agent.AgentAddressProvider;
+import org.apache.bifromq.basecluster.memberlist.agent.AgentMessenger;
+import org.apache.bifromq.basecluster.memberlist.agent.IAgent;
+import org.apache.bifromq.basecluster.membership.proto.Doubt;
+import org.apache.bifromq.basecluster.membership.proto.Fail;
+import org.apache.bifromq.basecluster.membership.proto.HostEndpoint;
+import org.apache.bifromq.basecluster.membership.proto.HostMember;
+import org.apache.bifromq.basecluster.membership.proto.Join;
+import org.apache.bifromq.basecluster.membership.proto.Quit;
+import org.apache.bifromq.basecluster.messenger.IMessenger;
+import org.apache.bifromq.basecluster.proto.ClusterMessage;
+import org.apache.bifromq.basecrdt.core.api.IORMap;
+import org.apache.bifromq.basecrdt.core.api.MVRegOperation;
+import org.apache.bifromq.basecrdt.core.api.ORMapOperation;
+import org.apache.bifromq.basecrdt.proto.Replica;
+import org.apache.bifromq.basecrdt.store.ICRDTStore;
+import org.apache.bifromq.basehlc.HLC;
+/**
+ * HostMemberList implementation using CRDT for achieving a consistent view of the host members in the cluster.
+ */
@Slf4j
public class HostMemberList implements IHostMemberList {
private final AtomicReference<State> state = new AtomicReference<>(State.JOINED);
@@ -79,12 +83,25 @@ public class HostMemberList implements IHostMemberList {
private final IHostAddressResolver addressResolver;
private final BehaviorSubject<Map<HostEndpoint, HostMember>> membershipSubject = BehaviorSubject.createDefault(
new ConcurrentHashMap<>());
+ private final PublishSubject<Long> refuteSubject = PublishSubject.create();
private final Map<String, Agent> agentMap = new ConcurrentHashMap<>();
private final IORMap hostListCRDT;
private final CompositeDisposable disposables = new CompositeDisposable();
private final MetricManager metricManager;
private final String[] tags;
private volatile HostMember local;
+
+ /**
+ * Constructor of HostMemberList.
+ *
+ * @param bindAddr the address to bind the host member
+ * @param port the port to bind the host member
+ * @param messenger the messenger to use for communication
+ * @param scheduler the scheduler to use for scheduling tasks
+ * @param store the CRDT store to use for storing internal OR-Map
+ * @param addressResolver the address resolver to resolve host endpoints to addresses
+ * @param tags the tags to be used for metrics
+ */
public HostMemberList(String bindAddr,
int port,
IMessenger messenger,
@@ -134,10 +151,13 @@ public class HostMemberList implements IHostMemberList {
if (joined) {
// add it into crdt
log.debug("Member[{}] joins the cluster: local={}", member, local);
- Optional<HostMember> memberInCRDT = getHostMember(hostListCRDT, member.getEndpoint());
- if (memberInCRDT.isEmpty() || memberInCRDT.get().getIncarnation() < member.getIncarnation()) {
- hostListCRDT.execute(ORMapOperation.update(member.getEndpoint().toByteString())
- .with(MVRegOperation.write(member.toByteString())));
+ if (member == local) {
+ // only update crdt if it's local member
+ Optional<HostMember> memberInCRDT = getHostMember(hostListCRDT, member.getEndpoint());
+ if (memberInCRDT.isEmpty() || memberInCRDT.get().getIncarnation() < member.getIncarnation()) {
+ hostListCRDT.execute(ORMapOperation.update(member.getEndpoint().toByteString())
+ .with(MVRegOperation.write(member.toByteString())));
+ }
}
// update crdt landscape
store.join(hostListCRDT.id(), currentMembers().keySet().stream()
@@ -148,12 +168,11 @@ public class HostMemberList implements IHostMemberList {
}
}
- private void drop(HostEndpoint memberEndpoint, int incarnation) {
+ private void drop(HostEndpoint memberEndpoint, int incarnation, boolean fromQuit) {
synchronized (this) {
boolean removed = removeMember(memberEndpoint, incarnation);
Optional<HostMember> memberInCRDT = getHostMember(hostListCRDT, memberEndpoint);
- if (memberInCRDT.isPresent()) {
- // remove it from crdt if any
+ if (!fromQuit && memberInCRDT.isPresent() && shouldReportFailure(memberInCRDT.get().getEndpoint())) {
hostListCRDT.execute(ORMapOperation.remove(memberEndpoint.toByteString()).of(mvreg));
}
if (removed) {
@@ -165,6 +184,17 @@ public class HostMemberList implements IHostMemberList {
}
}
+ private boolean shouldReportFailure(HostEndpoint failedMemberEndpoint) {
+ // if local member is responsible for removing the failed member from CRDT
+ RendezvousHash<HostEndpoint, HostEndpoint> hash = RendezvousHash.<HostEndpoint, HostEndpoint>builder()
+ .keyFunnel((from, into) -> into.putBytes(from.getId().asReadOnlyByteBuffer()))
+ .nodeFunnel((from, into) -> into.putBytes(from.getId().asReadOnlyByteBuffer()))
+ .nodes(currentMembers().keySet())
+ .build();
+ HostEndpoint reporter = hash.get(failedMemberEndpoint);
+ return reporter.getId().equals(local.getEndpoint().getId());
+ }
+
@Override
public boolean isZombie(HostEndpoint endpoint) {
return !endpoint.getId().equals(local.getEndpoint().getId())
@@ -207,6 +237,7 @@ public class HostMemberList implements IHostMemberList {
.thenCompose(v -> store.stopHosting(hostListCRDT.id()))
.whenComplete((v, e) -> {
membershipSubject.onComplete();
+ refuteSubject.onComplete();
metricManager.close();
state.set(State.QUITED);
});
@@ -226,6 +257,8 @@ public class HostMemberList implements IHostMemberList {
synchronized (this) {
local = local.toBuilder().setIncarnation(Math.max(local.getIncarnation(), atLeastIncarnation) + 1).build();
join(local);
+ agentMap.values().forEach(Agent::refreshRegistration);
+ refuteSubject.onNext(HLC.INST.get());
}
}
@@ -247,7 +280,6 @@ public class HostMemberList implements IHostMemberList {
tags));
local = local.toBuilder()
.setIncarnation(local.getIncarnation() + 1)
- .addAgentId(agentId) // deprecate since 3.3.3
.putAgent(agentId, agentEndpoint.getIncarnation())
.build();
join(local);
@@ -265,8 +297,6 @@ public class HostMemberList implements IHostMemberList {
synchronized (this) {
local = local.toBuilder()
.setIncarnation(local.getIncarnation() + 1)
- .clearAgentId()
- .addAllAgentId(agentMap.keySet()) // deprecate since 3.3.3
.clearAgent()
.putAllAgent(Maps.transformValues(agentMap, a -> a.local().getIncarnation()))
.build();
@@ -279,7 +309,12 @@ public class HostMemberList implements IHostMemberList {
@Override
public Observable<Map<HostEndpoint, Set<String>>> landscape() {
- return membershipSubject.map(m -> Maps.transformValues(m, v -> Sets.newHashSet(v.getAgentIdList())));
+ return membershipSubject.map(m -> Maps.transformValues(m, v -> v.getAgentMap().keySet()));
+ }
+
+ @Override
+ public Observable<Long> refuteSignal() {
+ return refuteSubject;
}
private Map<HostEndpoint, HostMember> currentMembers() {
@@ -327,6 +362,9 @@ public class HostMemberList implements IHostMemberList {
case QUIT -> handleQuit(message.getQuit());
case FAIL -> handleFail(message.getFail());
case DOUBT -> handleDoubt(message.getDoubt());
+ default -> {
+ // never happen
+ }
}
}
@@ -363,7 +401,7 @@ public class HostMemberList implements IHostMemberList {
} else if (isZombie(failedEndpoint)) {
clearZombie(failedEndpoint);
} else {
- drop(failedEndpoint, fail.getIncarnation());
+ drop(failedEndpoint, fail.getIncarnation(), false);
}
}
@@ -371,7 +409,7 @@ public class HostMemberList implements IHostMemberList {
HostEndpoint quitEndpoint = quit.getEndpoint();
log.debug("Member[{}] quits the cluster", quitEndpoint);
if (!quitEndpoint.equals(local.getEndpoint()) && !isZombie(quitEndpoint)) {
- drop(quitEndpoint, quit.getIncarnation());
+ drop(quitEndpoint, quit.getIncarnation(), true);
}
}
@@ -388,7 +426,7 @@ public class HostMemberList implements IHostMemberList {
private void clearZombie(HostEndpoint zombieEndpoint) {
// drop zombie if any, and broadcast a quit on behalf of it
- drop(zombieEndpoint, Integer.MAX_VALUE);
+ drop(zombieEndpoint, Integer.MAX_VALUE, false);
messenger.spread(ClusterMessage.newBuilder()
.setQuit(Quit.newBuilder().setEndpoint(zombieEndpoint).setIncarnation(Integer.MAX_VALUE).build())
.build());
diff --git a/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/IHostMemberList.java b/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/IHostMemberList.java
index c772a406..52b152fb 100644
--- a/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/IHostMemberList.java
+++ b/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/IHostMemberList.java
@@ -14,19 +14,22 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecluster.memberlist;
-import org.apache.bifromq.basecluster.memberlist.agent.IAgent;
-import org.apache.bifromq.basecluster.membership.proto.HostEndpoint;
-import org.apache.bifromq.basecluster.membership.proto.HostMember;
import io.reactivex.rxjava3.core.Observable;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
+import org.apache.bifromq.basecluster.memberlist.agent.IAgent;
+import org.apache.bifromq.basecluster.membership.proto.HostEndpoint;
+import org.apache.bifromq.basecluster.membership.proto.HostMember;
+/**
+ * The interface of host member list service.
+ */
public interface IHostMemberList {
/**
* The member from local.
@@ -35,6 +38,12 @@ public interface IHostMemberList {
*/
HostMember local();
+ /**
+ * If the given endpoint is considered a zombie(The dead endpoint used to live in the local host).
+ *
+ * @param endpoint the endpoint
+ * @return true if the given endpoint is considered a zombie.
+ */
boolean isZombie(HostEndpoint endpoint);
/**
@@ -71,4 +80,12 @@ public interface IHostMemberList {
* @return the observable
*/
Observable<Map<HostEndpoint, Set<String>>> landscape();
+
+ /**
+ * Emits a signal whenever the local member actively refutes a suspicion of being dead.
+ * Each emission carries the timestamp (in millis) when the refutation occurred.
+ *
+ * @return an observable stream of refutation timestamps
+ */
+ Observable<Long> refuteSignal();
}
diff --git a/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/Agent.java b/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/Agent.java
index 8a9e656f..cb46467c 100644
--- a/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/Agent.java
+++ b/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/Agent.java
@@ -14,21 +14,14 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecluster.memberlist.agent;
-import static org.apache.bifromq.basecrdt.core.api.CausalCRDTType.mvreg;
import static java.util.Collections.emptyMap;
+import static org.apache.bifromq.basecrdt.core.api.CausalCRDTType.mvreg;
-import org.apache.bifromq.basecluster.agent.proto.AgentEndpoint;
-import org.apache.bifromq.basecluster.agent.proto.AgentMemberAddr;
-import org.apache.bifromq.basecluster.agent.proto.AgentMemberMetadata;
-import org.apache.bifromq.basecrdt.core.api.IORMap;
-import org.apache.bifromq.basecrdt.core.api.ORMapOperation;
-import org.apache.bifromq.basecrdt.proto.Replica;
-import org.apache.bifromq.basecrdt.store.ICRDTStore;
import com.google.common.collect.Sets;
import com.google.protobuf.AbstractMessageLite;
import io.micrometer.core.instrument.Gauge;
@@ -38,7 +31,6 @@ import io.reactivex.rxjava3.core.Scheduler;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
@@ -50,13 +42,18 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.base.util.RendezvousHash;
+import org.apache.bifromq.basecluster.agent.proto.AgentEndpoint;
+import org.apache.bifromq.basecluster.agent.proto.AgentMemberAddr;
+import org.apache.bifromq.basecluster.agent.proto.AgentMemberMetadata;
+import org.apache.bifromq.basecluster.membership.proto.HostEndpoint;
+import org.apache.bifromq.basecrdt.core.api.IORMap;
+import org.apache.bifromq.basecrdt.core.api.ORMapOperation;
+import org.apache.bifromq.basecrdt.proto.Replica;
+import org.apache.bifromq.basecrdt.store.ICRDTStore;
@Slf4j
public final class Agent implements IAgent {
- private enum State {
- JOINED, QUITTING, QUITED
- }
-
private final ReadWriteLock quitLock = new ReentrantReadWriteLock();
private final String agentId;
private final AgentEndpoint localEndpoint;
@@ -70,7 +67,6 @@ public final class Agent implements IAgent {
BehaviorSubject.createDefault(emptyMap());
private final CompositeDisposable disposables = new CompositeDisposable();
private final Gauge memberNumGauge;
- private volatile Set<AgentEndpoint> currentAgentEndpoints = new HashSet<>();
public Agent(String agentId,
AgentEndpoint endpoint,
@@ -140,6 +136,11 @@ public final class Agent implements IAgent {
});
}
+ @Override
+ public void refreshRegistration() {
+ localMemberRegistry.values().forEach(AgentMember::refresh);
+ }
+
public CompletableFuture<Void> quit() {
Lock writeLock = quitLock.writeLock();
try {
@@ -184,27 +185,37 @@ public final class Agent implements IAgent {
private void handleAgentEndpointsUpdate(Set<AgentEndpoint> agentEndpoints) {
skipRunIfNotJoined(() -> {
- Set<AgentEndpoint> newAgentEndpoints = Sets.newHashSet(agentEndpoints);
- newAgentEndpoints.add(localEndpoint);
- Set<AgentEndpoint> leftHosts = Sets.difference(currentAgentEndpoints, newAgentEndpoints);
- // drop members on left hosts
+ Set<AgentEndpoint> aliveAgentEndpoints = Sets.newHashSet(agentEndpoints);
+ aliveAgentEndpoints.add(localEndpoint);
+ // compute alive endpoints from host member list (clean source of truth)
+ Set<HostEndpoint> aliveAgentHostEndpoints = aliveAgentEndpoints.stream()
+ .map(AgentEndpoint::getEndpoint)
+ .collect(Collectors.toSet());
+ // drop members in CRDT that are not present in alive host endpoints
Map<AgentMemberAddr, AgentMemberMetadata> agentMemberMap = CRDTUtil.toAgentMemberMap(agentCRDT);
for (AgentMemberAddr memberAddr : agentMemberMap.keySet()) {
- AgentEndpoint agentEndpoint = AgentEndpoint.newBuilder()
- .setEndpoint(memberAddr.getEndpoint())
- .setIncarnation(memberAddr.getIncarnation())
- .build();
- if (leftHosts.contains(agentEndpoint)) {
+ if (!aliveAgentHostEndpoints.contains(memberAddr.getEndpoint())
+ && shouldClean(aliveAgentEndpoints, memberAddr.getEndpoint())) {
agentCRDT.execute(ORMapOperation.remove(memberAddr.toByteString()).of(mvreg));
}
}
// update landscape
- currentAgentEndpoints = newAgentEndpoints;
store.join(agentCRDT.id(),
- currentAgentEndpoints.stream().map(AbstractMessageLite::toByteString).collect(Collectors.toSet()));
+ aliveAgentEndpoints.stream().map(AbstractMessageLite::toByteString).collect(Collectors.toSet()));
});
}
+ private boolean shouldClean(Set<AgentEndpoint> allEndpoints, HostEndpoint failedMemberEndpoint) {
+ // if local member is responsible for removing the failed member from CRDT
+ RendezvousHash<HostEndpoint, AgentEndpoint> hash = RendezvousHash.<HostEndpoint, AgentEndpoint>builder()
+ .keyFunnel((from, into) -> into.putBytes(from.getId().asReadOnlyByteBuffer()))
+ .nodeFunnel((from, into) -> into.putBytes(from.getEndpoint().getId().asReadOnlyByteBuffer()))
+ .nodes(allEndpoints)
+ .build();
+ AgentEndpoint cleaner = hash.get(failedMemberEndpoint);
+ return cleaner.getEndpoint().getId().equals(localEndpoint.getEndpoint().getId());
+ }
+
private void skipRunIfNotJoined(Runnable runnable) {
Lock readLock = quitLock.readLock();
try {
@@ -231,4 +242,8 @@ public final class Agent implements IAgent {
readLock.unlock();
}
}
+
+ private enum State {
+ JOINED, QUITTING, QUITED
+ }
}
diff --git a/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/AgentMember.java b/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/AgentMember.java
index e36980ab..6e5d6e77 100644
--- a/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/AgentMember.java
+++ b/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/AgentMember.java
@@ -14,21 +14,13 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecluster.memberlist.agent;
import static org.apache.bifromq.basecrdt.core.api.CausalCRDTType.mvreg;
-import org.apache.bifromq.basecluster.agent.proto.AgentMemberAddr;
-import org.apache.bifromq.basecluster.agent.proto.AgentMemberMetadata;
-import org.apache.bifromq.basecluster.agent.proto.AgentMessage;
-import org.apache.bifromq.basecluster.agent.proto.AgentMessageEnvelope;
-import org.apache.bifromq.basecrdt.core.api.IORMap;
-import org.apache.bifromq.basecrdt.core.api.MVRegOperation;
-import org.apache.bifromq.basecrdt.core.api.ORMapOperation;
-import org.apache.bifromq.basehlc.HLC;
import com.google.protobuf.ByteString;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Scheduler;
@@ -38,12 +30,23 @@ import java.net.UnknownHostException;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Supplier;
import java.util.stream.Collectors;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.basecluster.agent.proto.AgentMemberAddr;
+import org.apache.bifromq.basecluster.agent.proto.AgentMemberMetadata;
+import org.apache.bifromq.basecluster.agent.proto.AgentMessage;
+import org.apache.bifromq.basecluster.agent.proto.AgentMessageEnvelope;
+import org.apache.bifromq.basecrdt.core.api.IORMap;
+import org.apache.bifromq.basecrdt.core.api.MVRegOperation;
+import org.apache.bifromq.basecrdt.core.api.ORMapOperation;
+import org.apache.bifromq.basehlc.HLC;
+@Slf4j
class AgentMember implements IAgentMember {
private final AgentMemberAddr localAddr;
private final IORMap agentCRDT;
@@ -52,8 +55,9 @@ class AgentMember implements IAgentMember {
private final PublishSubject<AgentMessage> agentMessageSubject = PublishSubject.create();
private final CompositeDisposable disposables = new CompositeDisposable();
private final ReadWriteLock destroyLock = new ReentrantReadWriteLock();
+ private final AtomicReference<AgentMemberMetadata> metadata = new AtomicReference<>(
+ AgentMemberMetadata.newBuilder().setHlc(HLC.INST.get()).build());
private volatile boolean destroy = false;
- private volatile AgentMemberMetadata metadata;
AgentMember(AgentMemberAddr memberAddr,
IORMap agentCRDT,
@@ -64,7 +68,6 @@ class AgentMember implements IAgentMember {
this.agentCRDT = agentCRDT;
this.messenger = messenger;
this.memberAddresses = memberAddresses;
- metadata = AgentMemberMetadata.newBuilder().setHlc(HLC.INST.get()).build();
updateCRDT();
disposables.add(agentCRDT.inflation()
.observeOn(scheduler)
@@ -78,14 +81,14 @@ class AgentMember implements IAgentMember {
@Override
public AgentMemberMetadata metadata() {
- return metadata;
+ return metadata.get();
}
@Override
public void metadata(ByteString value) {
skipRunWhenDestroyed(() -> {
- if (!metadata.getValue().equals(value)) {
- metadata = AgentMemberMetadata.newBuilder().setValue(value).setHlc(HLC.INST.get()).build();
+ if (!metadata.get().getValue().equals(value)) {
+ metadata.set(AgentMemberMetadata.newBuilder().setValue(value).setHlc(HLC.INST.get()).build());
updateCRDT();
}
});
@@ -136,7 +139,7 @@ class AgentMember implements IAgentMember {
private void updateCRDT(long ts) {
skipRunWhenDestroyed(() -> {
Optional<AgentMemberMetadata> metaOnCRDT = CRDTUtil.getAgentMemberMetadata(agentCRDT, localAddr);
- if (metaOnCRDT.isEmpty() || !metaOnCRDT.get().equals(metadata)) {
+ if (metaOnCRDT.isEmpty() || !metaOnCRDT.get().equals(metadata.get())) {
updateCRDT();
}
});
@@ -144,7 +147,7 @@ class AgentMember implements IAgentMember {
private void updateCRDT() {
skipRunWhenDestroyed(() -> agentCRDT.execute(ORMapOperation.update(localAddr.toByteString())
- .with(MVRegOperation.write(metadata.toByteString()))));
+ .with(MVRegOperation.write(metadata.get().toByteString()))));
}
@Override
@@ -152,6 +155,14 @@ class AgentMember implements IAgentMember {
return agentMessageSubject;
}
+ @Override
+ public void refresh() {
+ skipRunWhenDestroyed(() -> {
+ metadata.set(metadata.get().toBuilder().setHlc(HLC.INST.get()).build());
+ updateCRDT();
+ });
+ }
+
private void skipRunWhenDestroyed(Runnable runnable) {
Lock readLock = destroyLock.readLock();
try {
diff --git a/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/IAgent.java b/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/IAgent.java
index dee044d5..87202e1a 100644
--- a/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/IAgent.java
+++ b/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/IAgent.java
@@ -14,19 +14,27 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecluster.memberlist.agent;
-import org.apache.bifromq.basecluster.agent.proto.AgentEndpoint;
-import org.apache.bifromq.basecluster.agent.proto.AgentMemberAddr;
-import org.apache.bifromq.basecluster.agent.proto.AgentMemberMetadata;
import io.reactivex.rxjava3.core.Observable;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
+import org.apache.bifromq.basecluster.agent.proto.AgentEndpoint;
+import org.apache.bifromq.basecluster.agent.proto.AgentMemberAddr;
+import org.apache.bifromq.basecluster.agent.proto.AgentMemberMetadata;
+/**
+ * The interface for an overlay agent cluster.
+ */
public interface IAgent {
+ /**
+ * The agent cluster id.
+ *
+ * @return the agent cluster id
+ */
String id();
/**
@@ -39,7 +47,7 @@ public interface IAgent {
/**
* A hot observable of agent membership.
*
- * @return
+ * @return an observable that emits the current membership map
*/
Observable<Map<AgentMemberAddr, AgentMemberMetadata>> membership();
@@ -47,14 +55,19 @@ public interface IAgent {
* Register a local agent member. It's allowed to register same member name in same logical agent from different
* agent hosts
*
- * @param memberName
+ * @param memberName the member name, should be unique in local host member
*/
IAgentMember register(String memberName);
/**
- * Deregister a member instance, the caller should never hold the reference to the instance after deregistered
+ * Deregister a member instance, the caller should never hold the reference to the instance after deregistered.
*
- * @param member
+ * @param member the member instance to deregister
*/
CompletableFuture<Void> deregister(IAgentMember member);
+
+ /**
+ * Refresh the registration of the local agent member.
+ */
+ void refreshRegistration();
}
diff --git a/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/IAgentMember.java b/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/IAgentMember.java
index 3d921d77..69e3a187 100644
--- a/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/IAgentMember.java
+++ b/base-cluster/src/main/java/org/apache/bifromq/basecluster/memberlist/agent/IAgentMember.java
@@ -14,17 +14,17 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecluster.memberlist.agent;
-import org.apache.bifromq.basecluster.agent.proto.AgentMemberAddr;
-import org.apache.bifromq.basecluster.agent.proto.AgentMemberMetadata;
-import org.apache.bifromq.basecluster.agent.proto.AgentMessage;
import com.google.protobuf.ByteString;
import io.reactivex.rxjava3.core.Observable;
import java.util.concurrent.CompletableFuture;
+import org.apache.bifromq.basecluster.agent.proto.AgentMemberAddr;
+import org.apache.bifromq.basecluster.agent.proto.AgentMemberMetadata;
+import org.apache.bifromq.basecluster.agent.proto.AgentMessage;
public interface IAgentMember {
AgentMemberAddr address();
@@ -32,50 +32,55 @@ public interface IAgentMember {
/**
* Broadcast a message among the agent members.
*
- * @param message
- * @param reliable
- * @return
+ * @param message the message to be sent
+ * @param reliable if true, the message will be sent reliably, otherwise it may be dropped
+ * @return a CompletableFuture that completes when the message is sent
*/
CompletableFuture<Void> broadcast(ByteString message, boolean reliable);
/**
- * Send a message to another member located in given endpoint
+ * Send a message to another member located in given endpoint.
*
- * @param targetMemberAddr
- * @param message
- * @param reliable
- * @return
+ * @param targetMemberAddr the address of the target member
+ * @param message the message to be sent
+ * @param reliable if true, the message will be sent reliably, otherwise it may be dropped
+ * @return a CompletableFuture that completes when the message is sent
*/
CompletableFuture<Void> send(AgentMemberAddr targetMemberAddr, ByteString message, boolean reliable);
/**
- * Send a message to all endpoints where target member name is registered
+ * Send a message to all endpoints where target member name is registered.
*
- * @param targetMemberName
- * @param message
- * @param reliable
- * @return
+ * @param targetMemberName the name of the target member
+ * @param message the message to be sent
+ * @param reliable if true, the message will be sent reliably, otherwise it may be dropped
+ * @return a CompletableFuture that completes when the message is sent
*/
CompletableFuture<Void> multicast(String targetMemberName, ByteString message, boolean reliable);
/**
- * Get current associated metadata
+ * Get current associated metadata.
*
- * @return
+ * @return the current metadata
*/
AgentMemberMetadata metadata();
/**
- * Update associated metadata
+ * Update associated metadata.
*
- * @param value
+ * @param value the new metadata value
*/
void metadata(ByteString value);
/**
- * An observable of incoming messages
+ * An observable of incoming messages.
*
- * @return
+ * @return an observable that emits AgentMessage
*/
Observable<AgentMessage> receive();
+
+ /**
+ * Refresh the registration of the local agent member.
+ */
+ void refresh();
}
diff --git a/base-cluster/src/main/proto/basecluster/membership/HostMember.proto b/base-cluster/src/main/proto/basecluster/membership/HostMember.proto
index 9d706d44..63b2f4d5 100644
--- a/base-cluster/src/main/proto/basecluster/membership/HostMember.proto
+++ b/base-cluster/src/main/proto/basecluster/membership/HostMember.proto
@@ -34,6 +34,6 @@ message HostEndpoint{
message HostMember {
HostEndpoint endpoint = 1;
uint32 incarnation = 2; // incarnation of the node, managed by the node itself
- repeated string agentId = 3; // deprecate since 3.3.3, the list of agents reside on the host
+ //repeated string agentId = 3; deprecate since 3.3.3, the list of agents reside on the host
map<string, uint64> agent = 4; // the map of agent id to incarnation
}
diff --git a/base-cluster/src/test/java/org/apache/bifromq/basecluster/AgentHostsTest.java b/base-cluster/src/test/java/org/apache/bifromq/basecluster/AgentHostsTest.java
index cce647ab..973609d5 100644
--- a/base-cluster/src/test/java/org/apache/bifromq/basecluster/AgentHostsTest.java
+++ b/base-cluster/src/test/java/org/apache/bifromq/basecluster/AgentHostsTest.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecluster;
@@ -22,6 +22,13 @@ package org.apache.bifromq.basecluster;
import static com.google.protobuf.ByteString.copyFromUtf8;
import static org.awaitility.Awaitility.await;
+import com.google.common.collect.Sets;
+import com.google.protobuf.ByteString;
+import io.reactivex.rxjava3.observers.TestObserver;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
import org.apache.bifromq.basecluster.agent.proto.AgentMemberAddr;
import org.apache.bifromq.basecluster.agent.proto.AgentMemberMetadata;
import org.apache.bifromq.basecluster.agent.proto.AgentMessage;
@@ -30,13 +37,6 @@ import org.apache.bifromq.basecluster.annotation.StoreCfgs;
import org.apache.bifromq.basecluster.memberlist.agent.IAgent;
import org.apache.bifromq.basecluster.memberlist.agent.IAgentMember;
import org.apache.bifromq.basecluster.membership.proto.HostEndpoint;
-import com.google.common.collect.Sets;
-import com.google.protobuf.ByteString;
-import io.reactivex.rxjava3.observers.TestObserver;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import lombok.extern.slf4j.Slf4j;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -86,16 +86,12 @@ public class AgentHostsTest extends AgentTestTemplate {
@StoreCfg(id = "s1", isSeed = true),
@StoreCfg(id = "s2"),
@StoreCfg(id = "s3"),
- @StoreCfg(id = "s4"),
- @StoreCfg(id = "s5"),
})
@Test
public void testMultipleAgentHosts() {
- await().until(() -> storeMgr.membership("s1").size() == 5);
- await().until(() -> storeMgr.membership("s2").size() == 5);
- await().until(() -> storeMgr.membership("s3").size() == 5);
- await().until(() -> storeMgr.membership("s4").size() == 5);
- await().until(() -> storeMgr.membership("s5").size() == 5);
+ await().forever().until(() -> storeMgr.membership("s1").size() == 3);
+ await().forever().until(() -> storeMgr.membership("s2").size() == 3);
+ await().forever().until(() -> storeMgr.membership("s3").size() == 3);
}
@Test
@@ -385,7 +381,7 @@ public class AgentHostsTest extends AgentTestTemplate {
await().until(() -> agentOnS2.membership().blockingFirst().size() == 4);
await().until(() -> agentOnS3.membership().blockingFirst().size() == 4);
- // isolate s1 from others
+ // isolate s2 from others
log.info("isolate s1");
storeMgr.isolate("s1");
await().forever().until(() -> agentOnS1.membership().blockingFirst().size() == 2);
@@ -395,8 +391,46 @@ public class AgentHostsTest extends AgentTestTemplate {
log.info("integrate s1");
// integrate s1 into the cluster
storeMgr.integrate("s1");
- await().until(() -> agentOnS1.membership().blockingFirst().size() == 4);
- await().until(() -> agentOnS2.membership().blockingFirst().size() == 4);
- await().until(() -> agentOnS3.membership().blockingFirst().size() == 4);
+ await().forever().until(() -> agentOnS1.membership().blockingFirst().size() == 4);
+ await().forever().until(() -> agentOnS2.membership().blockingFirst().size() == 4);
+ await().forever().until(() -> agentOnS3.membership().blockingFirst().size() == 4);
+ }
+
+ @StoreCfgs(stores = {
+ @StoreCfg(id = "s1", isSeed = true),
+ @StoreCfg(id = "s2"),
+ })
+ @Test
+ public void testCleanStaleAgentMembersAfterHostRestartWithNewEndpoint() {
+ // ensure cluster up
+ await().until(() -> storeMgr.membership("s1").size() == 2);
+ await().until(() -> storeMgr.membership("s2").size() == 2);
+
+ // host same agent on both hosts so CRDT survives while s1 restarts
+ IAgent agentOnS1 = storeMgr.hostAgent("s1", "agentX");
+ IAgent agentOnS2 = storeMgr.hostAgent("s2", "agentX");
+
+ // register a member only on s1 to create a CRDT entry bound to s1's endpoint
+ IAgentMember s1Member = agentOnS1.register("nodeOnS1");
+ s1Member.metadata(copyFromUtf8("payload"));
+
+ // both sides should observe exactly 1 member
+ await().until(() -> agentOnS1.membership().blockingFirst().size() == 1);
+ await().until(() -> agentOnS2.membership().blockingFirst().size() == 1);
+
+ storeMgr.crash("s1");
+ // s2 should eventually only see itself
+ await().forever().until(() -> storeMgr.membership("s2").size() == 1);
+
+ // start a new s1 instance with a new endpoint (old isolated one still exists but unreachable)
+ storeMgr.startHost("s1");
+ // rejoin cluster
+ storeMgr.join("s1", "s2");
+ // re-host the agent on s1 (no members registered now)
+ IAgent newAgentOnS1 = storeMgr.hostAgent("s1", "agentX");
+
+ // eventually, the stale member from old s1 endpoint should be cleaned from CRDT
+ await().until(() -> newAgentOnS1.membership().blockingFirst().isEmpty());
+ await().until(() -> agentOnS2.membership().blockingFirst().isEmpty());
}
}
diff --git a/base-cluster/src/test/java/org/apache/bifromq/basecluster/AgentTestCluster.java b/base-cluster/src/test/java/org/apache/bifromq/basecluster/AgentTestCluster.java
index c827dbcf..399dbd92 100644
--- a/base-cluster/src/test/java/org/apache/bifromq/basecluster/AgentTestCluster.java
+++ b/base-cluster/src/test/java/org/apache/bifromq/basecluster/AgentTestCluster.java
@@ -14,17 +14,11 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecluster;
-import org.apache.bifromq.basecluster.agent.proto.AgentMemberAddr;
-import org.apache.bifromq.basecluster.agent.proto.AgentMemberMetadata;
-import org.apache.bifromq.basecluster.memberlist.HostAddressResolver;
-import org.apache.bifromq.basecluster.memberlist.agent.IAgent;
-import org.apache.bifromq.basecluster.membership.proto.HostEndpoint;
-import org.apache.bifromq.basecluster.transport.ITransport;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
@@ -40,30 +34,31 @@ import java.util.Map;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.basecluster.agent.proto.AgentMemberAddr;
+import org.apache.bifromq.basecluster.agent.proto.AgentMemberMetadata;
+import org.apache.bifromq.basecluster.memberlist.HostAddressResolver;
+import org.apache.bifromq.basecluster.memberlist.agent.IAgent;
+import org.apache.bifromq.basecluster.membership.proto.HostEndpoint;
+import org.apache.bifromq.basecluster.transport.ITransport;
@Slf4j
public class AgentTestCluster {
- @AllArgsConstructor
- private static class AgentHostMeta {
- final AgentHostOptions options;
- }
-
private final MockNetwork network = new MockNetwork();
private final Map<String, AgentHostMeta> hostMetaMap = Maps.newConcurrentMap();
private final Map<String, HostEndpoint> hostEndpointMap = Maps.newConcurrentMap();
private final Map<String, ITransport> hostTransportMap = Maps.newConcurrentMap();
private final Map<HostEndpoint, IAgentHost> hostMap = Maps.newConcurrentMap();
private final Map<String, List<ByteString>> inflationLogs = Maps.newConcurrentMap();
+ private final Map<String, HostEndpoint> crashedHostEndpointMap = Maps.newConcurrentMap();
+ private final Map<String, ITransport> crashedHostTransportMap = Maps.newConcurrentMap();
+ private final Map<HostEndpoint, IAgentHost> crashedHostMap = Maps.newConcurrentMap();
private final CompositeDisposable disposables = new CompositeDisposable();
public AgentTestCluster() {
}
- public String newHost(String hostId, AgentHostOptions options) {
- hostMetaMap.computeIfAbsent(hostId, k -> {
- loadStore(hostId, options);
- return new AgentHostMeta(options);
- });
+ public String registerHost(String hostId, AgentHostOptions options) {
+ hostMetaMap.computeIfAbsent(hostId, k -> new AgentHostMeta(options));
return hostId;
}
@@ -94,11 +89,24 @@ public class AgentTestCluster {
network.isolate(hostTransportMap.get(hostId));
}
+ public void crash(String hostId) {
+ checkHost(hostId);
+ network.isolate(hostTransportMap.get(hostId));
+ inflationLogs.remove(hostId);
+
+ HostEndpoint crashedEndpoint = hostEndpointMap.remove(hostId);
+ crashedHostEndpointMap.put(hostId, crashedEndpoint);
+
+ IAgentHost crashedAgentHost = hostMap.remove(crashedEndpoint);
+ crashedHostMap.put(crashedEndpoint, crashedAgentHost);
+ ITransport transport = hostTransportMap.remove(hostId);
+ crashedHostTransportMap.put(hostId, transport);
+ }
+
public void integrate(String hostId) {
network.integrate(hostTransportMap.get(hostId));
}
-
public HostEndpoint endpoint(String hostId) {
checkHost(hostId);
return getHost(hostId).local();
@@ -145,6 +153,8 @@ public class AgentTestCluster {
public void shutdown() {
disposables.dispose();
hostEndpointMap.keySet().forEach(this::stopHost);
+ crashedHostTransportMap.keySet().forEach(hostId ->
+ crashedHostMap.remove(crashedHostEndpointMap.get(hostId)).close());
}
public IAgentHost getHost(String hostId) {
@@ -155,4 +165,9 @@ public class AgentTestCluster {
private void checkHost(String hostId) {
Preconditions.checkArgument(hostEndpointMap.containsKey(hostId));
}
+
+ @AllArgsConstructor
+ private static class AgentHostMeta {
+ final AgentHostOptions options;
+ }
}
diff --git a/base-cluster/src/test/java/org/apache/bifromq/basecluster/AgentTestTemplate.java b/base-cluster/src/test/java/org/apache/bifromq/basecluster/AgentTestTemplate.java
index 9bc5aa25..369cd941 100644
--- a/base-cluster/src/test/java/org/apache/bifromq/basecluster/AgentTestTemplate.java
+++ b/base-cluster/src/test/java/org/apache/bifromq/basecluster/AgentTestTemplate.java
@@ -14,17 +14,17 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecluster;
-import org.apache.bifromq.basecluster.annotation.StoreCfg;
-import org.apache.bifromq.basecluster.annotation.StoreCfgs;
-import org.apache.bifromq.basecrdt.store.CRDTStoreOptions;
import java.lang.reflect.Method;
import java.time.Duration;
import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.basecluster.annotation.StoreCfg;
+import org.apache.bifromq.basecluster.annotation.StoreCfgs;
+import org.apache.bifromq.basecrdt.store.CRDTStoreOptions;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
@@ -42,14 +42,16 @@ public abstract class AgentTestTemplate {
if (storeMgr != null) {
if (storeCfgs != null) {
for (StoreCfg cfg : storeCfgs.stores()) {
- storeMgr.newHost(cfg.id(), build(cfg));
+ storeMgr.registerHost(cfg.id(), build(cfg));
+ storeMgr.startHost(cfg.id());
if (cfg.isSeed()) {
seedStoreId = cfg.id();
}
}
}
if (storeCfg != null) {
- storeMgr.newHost(storeCfg.id(), build(storeCfg));
+ storeMgr.registerHost(storeCfg.id(), build(storeCfg));
+ storeMgr.startHost(storeCfg.id());
}
if (seedStoreId != null && storeCfgs != null) {
for (StoreCfg cfg : storeCfgs.stores()) {
diff --git a/base-cluster/src/test/java/org/apache/bifromq/basecluster/memberlist/HostMemberListTest.java b/base-cluster/src/test/java/org/apache/bifromq/basecluster/memberlist/HostMemberListTest.java
index d51b20da..0a93cf4c 100644
--- a/base-cluster/src/test/java/org/apache/bifromq/basecluster/memberlist/HostMemberListTest.java
+++ b/base-cluster/src/test/java/org/apache/bifromq/basecluster/memberlist/HostMemberListTest.java
@@ -14,11 +14,12 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecluster.memberlist;
+import static java.util.Collections.emptyIterator;
import static org.apache.bifromq.basecluster.memberlist.CRDTUtil.AGENT_HOST_MAP_URI;
import static org.apache.bifromq.basecluster.memberlist.Fixtures.LOCAL_ADDR;
import static org.apache.bifromq.basecluster.memberlist.Fixtures.LOCAL_ENDPOINT;
@@ -27,7 +28,6 @@ import static org.apache.bifromq.basecluster.memberlist.Fixtures.LOCAL_STORE_ID;
import static org.apache.bifromq.basecluster.memberlist.Fixtures.REMOTE_ADDR_1;
import static org.apache.bifromq.basecluster.memberlist.Fixtures.REMOTE_HOST_1_ENDPOINT;
import static org.apache.bifromq.basecluster.memberlist.Fixtures.ZOMBIE_ENDPOINT;
-import static java.util.Collections.emptyIterator;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mockConstruction;
@@ -39,6 +39,18 @@ import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
+import com.google.common.collect.Iterators;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.protobuf.ByteString;
+import io.reactivex.rxjava3.core.Scheduler;
+import io.reactivex.rxjava3.schedulers.Schedulers;
+import io.reactivex.rxjava3.schedulers.Timed;
+import io.reactivex.rxjava3.subjects.PublishSubject;
+import java.net.InetSocketAddress;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import lombok.extern.slf4j.Slf4j;
import org.apache.bifromq.basecluster.memberlist.agent.Agent;
import org.apache.bifromq.basecluster.memberlist.agent.IAgent;
import org.apache.bifromq.basecluster.membership.proto.Doubt;
@@ -55,18 +67,6 @@ import org.apache.bifromq.basecrdt.core.api.IMVReg;
import org.apache.bifromq.basecrdt.core.api.IORMap;
import org.apache.bifromq.basecrdt.core.api.ORMapOperation;
import org.apache.bifromq.basecrdt.store.ICRDTStore;
-import com.google.common.collect.Iterators;
-import com.google.common.util.concurrent.MoreExecutors;
-import com.google.protobuf.ByteString;
-import io.reactivex.rxjava3.core.Scheduler;
-import io.reactivex.rxjava3.schedulers.Schedulers;
-import io.reactivex.rxjava3.schedulers.Timed;
-import io.reactivex.rxjava3.subjects.PublishSubject;
-import java.net.InetSocketAddress;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeUnit;
-import lombok.extern.slf4j.Slf4j;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockedConstruction;
@@ -120,7 +120,7 @@ public class HostMemberListTest {
assertEquals(local.getEndpoint().getAddress(), LOCAL_ADDR.getHostName());
assertEquals(local.getEndpoint().getPort(), LOCAL_ADDR.getPort());
assertTrue(local.getIncarnation() >= 0);
- assertTrue(local.getAgentIdList().isEmpty());
+ assertTrue(local.getAgentMap().isEmpty());
assertEquals(memberList.landscape().blockingFirst().size(), 1);
Map<HostEndpoint, Integer> hostMap = memberList.members().blockingFirst();
assertEquals(hostMap.size(), 1);
@@ -157,15 +157,15 @@ public class HostMemberListTest {
when(hostMemberOnCRDT.read()).thenReturn(emptyIterator());
IHostMemberList memberList = new HostMemberList(LOCAL_ADDR.getHostName(), LOCAL_ADDR.getPort(),
messenger, scheduler, store, addressResolver);
- HostMember local = memberList.local();
memberList.host(agentId);
when(mockAgent.constructed().get(0).quit()).thenReturn(CompletableFuture.completedFuture(null));
memberList.stopHosting(agentId);
- assertEquals(memberList.local().getAgentIdCount(), 0);
+ assertEquals(memberList.local().getAgentMap().size(), 0);
assertEquals(memberList.landscape().blockingFirst().size(), 1);
- assertTrue(local.getIncarnation() + 2 == memberList.local().getIncarnation());
+ HostMember local = memberList.local();
+ assertEquals(memberList.local().getIncarnation(), local.getIncarnation());
Map<HostEndpoint, Integer> hostMap = memberList.members().blockingFirst();
- assertTrue(local.getIncarnation() + 2 == hostMap.get(local.getEndpoint()));
+ assertEquals((int) hostMap.get(local.getEndpoint()), local.getIncarnation());
verify(hostListCRDT, times(3)).execute(any(ORMapOperation.ORMapUpdate.class));
}
@@ -197,7 +197,7 @@ public class HostMemberListTest {
.build()));
ArgumentCaptor<ORMapOperation> opCap = ArgumentCaptor.forClass(ORMapOperation.class);
- verify(hostListCRDT, times(2)).execute(opCap.capture());
+ verify(hostListCRDT, times(1)).execute(opCap.capture());
verify(store, times(2)).join(
argThat(r -> r.getUri().equals(AGENT_HOST_MAP_URI) && r.getId().equals(LOCAL_STORE_ID)), any());
}
@@ -243,7 +243,7 @@ public class HostMemberListTest {
.build(), LOCAL_ENDPOINT));
ArgumentCaptor<ORMapOperation> opCap = ArgumentCaptor.forClass(ORMapOperation.class);
- verify(hostListCRDT, times(2)).execute(opCap.capture());
+ verify(hostListCRDT, times(1)).execute(opCap.capture());
ArgumentCaptor<ClusterMessage> msgCap = ArgumentCaptor.forClass(ClusterMessage.class);
ArgumentCaptor<InetSocketAddress> addrCap = ArgumentCaptor.forClass(InetSocketAddress.class);
@@ -274,7 +274,7 @@ public class HostMemberListTest {
.build(), LOCAL_ENDPOINT));
ArgumentCaptor<ORMapOperation> opCap = ArgumentCaptor.forClass(ORMapOperation.class);
- verify(hostListCRDT, times(3)).execute(opCap.capture());
+ verify(hostListCRDT, times(2)).execute(opCap.capture());
ArgumentCaptor<ClusterMessage> msgCap = ArgumentCaptor.forClass(ClusterMessage.class);
ArgumentCaptor<InetSocketAddress> addrCap = ArgumentCaptor.forClass(InetSocketAddress.class);
@@ -333,9 +333,9 @@ public class HostMemberListTest {
messageSubject.onNext(failMsg(REMOTE_HOST_1_ENDPOINT, 1));
ArgumentCaptor<ORMapOperation> opCap = ArgumentCaptor.forClass(ORMapOperation.class);
- verify(hostListCRDT, times(3)).execute(opCap.capture());
- assertEquals(((ORMapOperation.ORMapRemove) opCap.getAllValues().get(2)).valueType, CausalCRDTType.mvreg);
- assertEquals(opCap.getAllValues().get(2).keyPath[0], REMOTE_HOST_1_ENDPOINT.toByteString());
+ verify(hostListCRDT, times(2)).execute(opCap.capture());
+ assertEquals(((ORMapOperation.ORMapRemove) opCap.getAllValues().get(1)).valueType, CausalCRDTType.mvreg);
+ assertEquals(opCap.getAllValues().get(1).keyPath[0], REMOTE_HOST_1_ENDPOINT.toByteString());
}
@Test
@@ -346,7 +346,6 @@ public class HostMemberListTest {
messenger, scheduler, store, addressResolver);
assertEquals(memberList.members().blockingFirst().get(LOCAL_ENDPOINT).intValue(), 0);
-
messageSubject.onNext(failMsg(LOCAL_ENDPOINT, 0));
messageSubject.onNext(failMsg(LOCAL_ENDPOINT, 0)); // this time will be ignored
@@ -388,7 +387,7 @@ public class HostMemberListTest {
messageSubject.onNext(quitMsg(REMOTE_HOST_1_ENDPOINT, 1));
// nothing will happen
- verify(hostListCRDT, times(1)).execute(any(ORMapOperation.ORMapRemove.class));
+ verify(hostListCRDT, never()).execute(any(ORMapOperation.ORMapRemove.class));
verify(store, times(1)).join(
argThat(r -> r.getUri().equals(AGENT_HOST_MAP_URI) && r.getId().equals(LOCAL_STORE_ID)), any());
}
@@ -409,7 +408,6 @@ public class HostMemberListTest {
argThat(r -> r.getUri().equals(AGENT_HOST_MAP_URI) && r.getId().equals(LOCAL_STORE_ID)), any());
}
-
@Test
public void handleQuitSelf() {
when(hostListCRDT.getMVReg(any())).thenReturn(hostMemberOnCRDT);
@@ -444,8 +442,8 @@ public class HostMemberListTest {
messageSubject.onNext(quitMsg(REMOTE_HOST_1_ENDPOINT, 0));
// nothing will happen
ArgumentCaptor<ORMapOperation> opCap = ArgumentCaptor.forClass(ORMapOperation.class);
- verify(hostListCRDT, times(3)).execute(opCap.capture());
- assertTrue(opCap.getAllValues().get(2) instanceof ORMapOperation.ORMapRemove);
+ verify(hostListCRDT, times(1)).execute(opCap.capture());
+ assertFalse(opCap.getAllValues().get(0) instanceof ORMapOperation.ORMapRemove);
verify(store, times(3)).join(
argThat(r -> r.getUri().equals(AGENT_HOST_MAP_URI) && r.getId().equals(LOCAL_STORE_ID)), any());
@@ -480,7 +478,6 @@ public class HostMemberListTest {
assertEquals(memberList.members().blockingFirst().get(LOCAL_ENDPOINT).intValue(), 0);
}
-
private Timed<MessageEnvelope> joinMsg(HostMember member) {
return to(ClusterMessage.newBuilder()
.setJoin(Join.newBuilder()
diff --git a/base-crdt/base-crdt-service/pom.xml b/base-crdt/base-crdt-service/pom.xml
index f0b2351a..2dd7cf2d 100644
--- a/base-crdt/base-crdt-service/pom.xml
+++ b/base-crdt/base-crdt-service/pom.xml
@@ -47,6 +47,10 @@
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- for testing-->
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ </dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
diff --git a/base-crdt/base-crdt-service/src/main/java/org/apache/bifromq/basecrdt/service/CRDTCluster.java b/base-crdt/base-crdt-service/src/main/java/org/apache/bifromq/basecrdt/service/CRDTCluster.java
index 72b13ae9..25eb0c82 100644
--- a/base-crdt/base-crdt-service/src/main/java/org/apache/bifromq/basecrdt/service/CRDTCluster.java
+++ b/base-crdt/base-crdt-service/src/main/java/org/apache/bifromq/basecrdt/service/CRDTCluster.java
@@ -73,7 +73,7 @@ class CRDTCluster<O extends ICRDTOperation, C extends ICausalCRDT<O>> {
this.store = store;
this.agentHost = agentHost;
replicaId = generate(uri);
- log = MDCLogger.getLogger(CRDTCluster.class, "replica", print(replicaId));
+ log = MDCLogger.getLogger(CRDTCluster.class, "store", store.id(), "replica", print(replicaId));
membershipAgent = agentHost.host(replicaId.getUri());
endpoint = AgentMemberAddr.newBuilder()
.setName(AgentUtil.toAgentMemberName(replicaId))
diff --git a/base-crdt/base-crdt-service/src/main/java/org/apache/bifromq/basecrdt/service/CRDTService.java b/base-crdt/base-crdt-service/src/main/java/org/apache/bifromq/basecrdt/service/CRDTService.java
index 94213d86..e74ee85d 100644
--- a/base-crdt/base-crdt-service/src/main/java/org/apache/bifromq/basecrdt/service/CRDTService.java
+++ b/base-crdt/base-crdt-service/src/main/java/org/apache/bifromq/basecrdt/service/CRDTService.java
@@ -115,6 +115,11 @@ public class CRDTService implements ICRDTService {
});
}
+ @Override
+ public Observable<Long> refreshSignal() {
+ return agentHost.refuteSignal();
+ }
+
private CompletableFuture<Void> stopHostingInternal(String uri) {
return hostedCRDT.remove(uri).close();
}
diff --git a/base-crdt/base-crdt-service/src/main/java/org/apache/bifromq/basecrdt/service/ICRDTService.java b/base-crdt/base-crdt-service/src/main/java/org/apache/bifromq/basecrdt/service/ICRDTService.java
index 14b1d72d..97375ff3 100644
--- a/base-crdt/base-crdt-service/src/main/java/org/apache/bifromq/basecrdt/service/ICRDTService.java
+++ b/base-crdt/base-crdt-service/src/main/java/org/apache/bifromq/basecrdt/service/ICRDTService.java
@@ -14,20 +14,20 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecrdt.service;
-import org.apache.bifromq.basecluster.IAgentHost;
-import org.apache.bifromq.basecrdt.core.api.ICRDTOperation;
-import org.apache.bifromq.basecrdt.core.api.ICausalCRDT;
-import org.apache.bifromq.basecrdt.proto.Replica;
import com.google.protobuf.ByteString;
import io.reactivex.rxjava3.core.Observable;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import lombok.NonNull;
+import org.apache.bifromq.basecluster.IAgentHost;
+import org.apache.bifromq.basecrdt.core.api.ICRDTOperation;
+import org.apache.bifromq.basecrdt.core.api.ICausalCRDT;
+import org.apache.bifromq.basecrdt.proto.Replica;
/**
* The CRDT service with decentralized membership management based on base-cluster.
@@ -89,6 +89,13 @@ public interface ICRDTService extends AutoCloseable {
*/
Observable<Set<String>> aliveCRDTs();
+ /**
+ * A signal to refresh the CRDT replica hosted in the service.
+ *
+ * @return an observable that emits refresh signal
+ */
+ Observable<Long> refreshSignal();
+
/**
* Stop the store.
*/
diff --git a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/AWORSetInflater.java b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/AWORSetInflater.java
index d3662f97..0d91e841 100644
--- a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/AWORSetInflater.java
+++ b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/AWORSetInflater.java
@@ -19,21 +19,22 @@
package org.apache.bifromq.basecrdt.core.internal;
+import java.time.Duration;
+import java.util.concurrent.ScheduledExecutorService;
import org.apache.bifromq.basecrdt.core.api.AWORSetOperation;
import org.apache.bifromq.basecrdt.core.api.CausalCRDTType;
import org.apache.bifromq.basecrdt.core.api.IAWORSet;
import org.apache.bifromq.basecrdt.core.api.IAWORSetInflater;
import org.apache.bifromq.basecrdt.proto.Replica;
-import java.time.Duration;
-import java.util.concurrent.ScheduledExecutorService;
class AWORSetInflater extends CausalCRDTInflater<IDotMap, AWORSetOperation, IAWORSet> implements IAWORSetInflater {
- AWORSetInflater(Replica replica,
+ AWORSetInflater(String storeId,
+ Replica replica,
IReplicaStateLattice stateLattice,
ScheduledExecutorService executor,
Duration inflationInterval,
String... tags) {
- super(replica, stateLattice, executor, inflationInterval, tags);
+ super(storeId, replica, stateLattice, executor, inflationInterval, tags);
}
@Override
diff --git a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/CCounterInflater.java b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/CCounterInflater.java
index 4bef5345..06c7e581 100644
--- a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/CCounterInflater.java
+++ b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/CCounterInflater.java
@@ -19,21 +19,22 @@
package org.apache.bifromq.basecrdt.core.internal;
+import java.time.Duration;
+import java.util.concurrent.ScheduledExecutorService;
import org.apache.bifromq.basecrdt.core.api.CCounterOperation;
import org.apache.bifromq.basecrdt.core.api.CausalCRDTType;
import org.apache.bifromq.basecrdt.core.api.ICCounter;
import org.apache.bifromq.basecrdt.core.api.ICCounterInflater;
import org.apache.bifromq.basecrdt.proto.Replica;
-import java.time.Duration;
-import java.util.concurrent.ScheduledExecutorService;
class CCounterInflater extends CausalCRDTInflater<IDotMap, CCounterOperation, ICCounter> implements ICCounterInflater {
- CCounterInflater(Replica replica,
+ CCounterInflater(String storeId,
+ Replica replica,
IReplicaStateLattice stateLattice,
ScheduledExecutorService executor,
Duration inflationInterval,
String... tags) {
- super(replica, stateLattice, executor, inflationInterval, tags);
+ super(storeId, replica, stateLattice, executor, inflationInterval, tags);
}
@Override
diff --git a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/CausalCRDTInflater.java b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/CausalCRDTInflater.java
index f38d8bd0..ba9e912c 100644
--- a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/CausalCRDTInflater.java
+++ b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/CausalCRDTInflater.java
@@ -73,11 +73,13 @@ abstract class CausalCRDTInflater<D extends IDotStore, O extends ICRDTOperation,
private volatile OperationExecTask currentOp = null;
private volatile JoinTask currentJoin = null;
- CausalCRDTInflater(Replica replica, IReplicaStateLattice stateLattice,
+ CausalCRDTInflater(String storeId,
+ Replica replica,
+ IReplicaStateLattice stateLattice,
ScheduledExecutorService executor,
Duration inflationInterval, String... tags) {
this.replica = replica;
- this.log = MDCLogger.getLogger(CausalCRDTInflater.class, "replica", print(replica));
+ this.log = MDCLogger.getLogger(CausalCRDTInflater.class, "store", storeId, "replica", print(replica));
this.replicaStateLattice = stateLattice;
this.executor = executor;
this.inflationInterval = inflationInterval;
diff --git a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/CausalCRDTInflaterFactory.java b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/CausalCRDTInflaterFactory.java
index 75aaa132..2428905c 100644
--- a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/CausalCRDTInflaterFactory.java
+++ b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/CausalCRDTInflaterFactory.java
@@ -19,27 +19,30 @@
package org.apache.bifromq.basecrdt.core.internal;
+import java.time.Duration;
+import java.util.concurrent.ScheduledExecutorService;
import org.apache.bifromq.basecrdt.core.api.CRDTURI;
import org.apache.bifromq.basecrdt.core.api.ICausalCRDTInflater;
import org.apache.bifromq.basecrdt.proto.Replica;
-import java.time.Duration;
-import java.util.concurrent.ScheduledExecutorService;
/**
* A factory for creating {@link CausalCRDTInflater} instances.
*/
public class CausalCRDTInflaterFactory {
+ private final String storeId;
private final ScheduledExecutorService executor;
private final Duration inflationInterval;
private final Duration orHistoryExpiryTime;
private final Duration maxCompactionTime;
private final String[] tags;
- public CausalCRDTInflaterFactory(Duration inflationInterval,
+ public CausalCRDTInflaterFactory(String storeId,
+ Duration inflationInterval,
Duration orHistoryExpiryTime,
Duration maxCompactionTime,
ScheduledExecutorService executor,
String... tags) {
+ this.storeId = storeId;
this.executor = executor;
this.inflationInterval = inflationInterval;
this.orHistoryExpiryTime = orHistoryExpiryTime;
@@ -57,16 +60,16 @@ public class CausalCRDTInflaterFactory {
CRDTURI.checkURI(replicaId.getUri());
IReplicaStateLattice lattice =
- new InMemReplicaStateLattice(replicaId, orHistoryExpiryTime, maxCompactionTime);
+ new InMemReplicaStateLattice(storeId, replicaId, orHistoryExpiryTime, maxCompactionTime);
return switch (CRDTURI.parseType(replicaId.getUri())) {
- case aworset -> new AWORSetInflater(replicaId, lattice, executor, inflationInterval, tags);
- case rworset -> new RWORSetInflater(replicaId, lattice, executor, inflationInterval, tags);
- case ormap -> new ORMapInflater(replicaId, lattice, executor, inflationInterval, tags);
- case cctr -> new CCounterInflater(replicaId, lattice, executor, inflationInterval, tags);
- case dwflag -> new DWFlagInflater(replicaId, lattice, executor, inflationInterval, tags);
- case ewflag -> new EWFlagInflater(replicaId, lattice, executor, inflationInterval, tags);
- case mvreg -> new MVRegInflater(replicaId, lattice, executor, inflationInterval, tags);
+ case aworset -> new AWORSetInflater(storeId, replicaId, lattice, executor, inflationInterval, tags);
+ case rworset -> new RWORSetInflater(storeId, replicaId, lattice, executor, inflationInterval, tags);
+ case ormap -> new ORMapInflater(storeId, replicaId, lattice, executor, inflationInterval, tags);
+ case cctr -> new CCounterInflater(storeId, replicaId, lattice, executor, inflationInterval, tags);
+ case dwflag -> new DWFlagInflater(storeId, replicaId, lattice, executor, inflationInterval, tags);
+ case ewflag -> new EWFlagInflater(storeId, replicaId, lattice, executor, inflationInterval, tags);
+ case mvreg -> new MVRegInflater(storeId, replicaId, lattice, executor, inflationInterval, tags);
};
}
}
diff --git a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/DWFlagInflater.java b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/DWFlagInflater.java
index bd8a654e..af8276cc 100644
--- a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/DWFlagInflater.java
+++ b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/DWFlagInflater.java
@@ -19,21 +19,22 @@
package org.apache.bifromq.basecrdt.core.internal;
+import java.time.Duration;
+import java.util.concurrent.ScheduledExecutorService;
import org.apache.bifromq.basecrdt.core.api.CausalCRDTType;
import org.apache.bifromq.basecrdt.core.api.DWFlagOperation;
import org.apache.bifromq.basecrdt.core.api.IDWFlag;
import org.apache.bifromq.basecrdt.core.api.IDWFlagInflater;
import org.apache.bifromq.basecrdt.proto.Replica;
-import java.time.Duration;
-import java.util.concurrent.ScheduledExecutorService;
class DWFlagInflater extends CausalCRDTInflater<IDotSet, DWFlagOperation, IDWFlag> implements IDWFlagInflater {
- DWFlagInflater(Replica replica,
+ DWFlagInflater(String storeId,
+ Replica replica,
IReplicaStateLattice stateLattice,
ScheduledExecutorService executor,
Duration inflationInterval,
String... tags) {
- super(replica, stateLattice, executor, inflationInterval, tags);
+ super(storeId, replica, stateLattice, executor, inflationInterval, tags);
}
@Override
diff --git a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/EWFlagInflater.java b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/EWFlagInflater.java
index e48b9464..dbcd8095 100644
--- a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/EWFlagInflater.java
+++ b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/EWFlagInflater.java
@@ -19,18 +19,18 @@
package org.apache.bifromq.basecrdt.core.internal;
+import java.time.Duration;
+import java.util.concurrent.ScheduledExecutorService;
import org.apache.bifromq.basecrdt.core.api.CausalCRDTType;
import org.apache.bifromq.basecrdt.core.api.EWFlagOperation;
import org.apache.bifromq.basecrdt.core.api.IEWFlag;
import org.apache.bifromq.basecrdt.core.api.IEWFlagInflater;
import org.apache.bifromq.basecrdt.proto.Replica;
-import java.time.Duration;
-import java.util.concurrent.ScheduledExecutorService;
class EWFlagInflater extends CausalCRDTInflater<IDotSet, EWFlagOperation, IEWFlag> implements IEWFlagInflater {
- EWFlagInflater(Replica replica, IReplicaStateLattice stateLattice,
+ EWFlagInflater(String storeId, Replica replica, IReplicaStateLattice stateLattice,
ScheduledExecutorService executor, Duration inflationInterval, String... tags) {
- super(replica, stateLattice, executor, inflationInterval, tags);
+ super(storeId, replica, stateLattice, executor, inflationInterval, tags);
}
@Override
diff --git a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/InMemReplicaStateLattice.java b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/InMemReplicaStateLattice.java
index d12fa320..0b42d7c5 100644
--- a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/InMemReplicaStateLattice.java
+++ b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/InMemReplicaStateLattice.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecrdt.core.internal;
@@ -69,9 +69,10 @@ class InMemReplicaStateLattice implements IReplicaStateLattice {
private final Duration historyExpire;
private final long maxCompactionDuration;
- InMemReplicaStateLattice(Replica ownerReplica, Duration historyExpire, Duration maxCompactionTime) {
+ InMemReplicaStateLattice(String storeId, Replica ownerReplica, Duration historyExpire, Duration maxCompactionTime) {
this.ownerReplica = ownerReplica;
- this.log = MDCLogger.getLogger(InMemReplicaStateLattice.class, "replica", print(ownerReplica));
+ this.log = MDCLogger.getLogger(InMemReplicaStateLattice.class,
+ "store", storeId, "replica", print(ownerReplica));
this.historyExpire = historyExpire;
this.maxCompactionDuration = maxCompactionTime.toNanos();
}
diff --git a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/MVRegInflater.java b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/MVRegInflater.java
index 3044da2f..497844b6 100644
--- a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/MVRegInflater.java
+++ b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/MVRegInflater.java
@@ -14,26 +14,27 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecrdt.core.internal;
+import java.time.Duration;
+import java.util.concurrent.ScheduledExecutorService;
import org.apache.bifromq.basecrdt.core.api.CausalCRDTType;
import org.apache.bifromq.basecrdt.core.api.IMVReg;
import org.apache.bifromq.basecrdt.core.api.IMVRegInflater;
import org.apache.bifromq.basecrdt.core.api.MVRegOperation;
import org.apache.bifromq.basecrdt.proto.Replica;
-import java.time.Duration;
-import java.util.concurrent.ScheduledExecutorService;
class MVRegInflater extends CausalCRDTInflater<IDotFunc, MVRegOperation, IMVReg> implements IMVRegInflater {
- MVRegInflater(Replica replica,
+ MVRegInflater(String storeId,
+ Replica replica,
IReplicaStateLattice stateLattice,
ScheduledExecutorService executor,
Duration inflationInterval,
String... tags) {
- super(replica, stateLattice, executor, inflationInterval, tags);
+ super(storeId, replica, stateLattice, executor, inflationInterval, tags);
}
@Override
diff --git a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/ORMapInflater.java b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/ORMapInflater.java
index 137b6234..aa8bec0b 100644
--- a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/ORMapInflater.java
+++ b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/ORMapInflater.java
@@ -14,26 +14,27 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecrdt.core.internal;
+import java.time.Duration;
+import java.util.concurrent.ScheduledExecutorService;
import org.apache.bifromq.basecrdt.core.api.CausalCRDTType;
import org.apache.bifromq.basecrdt.core.api.IORMap;
import org.apache.bifromq.basecrdt.core.api.IORMapInflater;
import org.apache.bifromq.basecrdt.core.api.ORMapOperation;
import org.apache.bifromq.basecrdt.proto.Replica;
-import java.time.Duration;
-import java.util.concurrent.ScheduledExecutorService;
class ORMapInflater extends CausalCRDTInflater<IDotMap, ORMapOperation, IORMap> implements IORMapInflater {
- ORMapInflater(Replica replica,
+ ORMapInflater(String storeId,
+ Replica replica,
IReplicaStateLattice stateLattice,
ScheduledExecutorService executor,
Duration inflationInterval,
String... tags) {
- super(replica, stateLattice, executor, inflationInterval, tags);
+ super(storeId, replica, stateLattice, executor, inflationInterval, tags);
}
@Override
diff --git a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/RWORSetInflater.java b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/RWORSetInflater.java
index 8698aa25..c842a58a 100644
--- a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/RWORSetInflater.java
+++ b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/core/internal/RWORSetInflater.java
@@ -14,26 +14,27 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecrdt.core.internal;
+import java.time.Duration;
+import java.util.concurrent.ScheduledExecutorService;
import org.apache.bifromq.basecrdt.core.api.CausalCRDTType;
import org.apache.bifromq.basecrdt.core.api.IRWORSet;
import org.apache.bifromq.basecrdt.core.api.IRWORSetInflater;
import org.apache.bifromq.basecrdt.core.api.RWORSetOperation;
import org.apache.bifromq.basecrdt.proto.Replica;
-import java.time.Duration;
-import java.util.concurrent.ScheduledExecutorService;
class RWORSetInflater extends CausalCRDTInflater<IDotMap, RWORSetOperation, IRWORSet> implements IRWORSetInflater {
- RWORSetInflater(Replica replica,
+ RWORSetInflater(String storeId,
+ Replica replica,
IReplicaStateLattice stateLattice,
ScheduledExecutorService executor,
Duration inflationInterval,
String... tags) {
- super(replica, stateLattice, executor, inflationInterval, tags);
+ super(storeId, replica, stateLattice, executor, inflationInterval, tags);
}
@Override
diff --git a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/store/AntiEntropy.java b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/store/AntiEntropy.java
index dff26bf6..6e2a39f1 100644
--- a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/store/AntiEntropy.java
+++ b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/store/AntiEntropy.java
@@ -65,6 +65,9 @@ final class AntiEntropy {
private long currentNeighborVer;
private long currentInflationTs;
private DeltaMessage currentDelta = null;
+ // track if the last sent delta contains replacements (i.e., real diff),
+ // so that after ACK we can proactively continue to drain remaining deltas
+ private boolean lastSentHasReplacement = false;
AntiEntropy(String storeId,
ByteString localAddr,
@@ -110,31 +113,51 @@ final class AntiEntropy {
}
void handleAck(AckMessage ack) {
- if (canceled.get() || !running.get()) {
+ if (canceled.get()) {
return;
}
synchronized (this) {
- if (!running.get() || currentDelta == null) {
- return;
- }
- if (ack.getSeqNo() != currentDelta.getSeqNo()) {
+ // Case 1: Matched ACK for in-flight delta
+ if (running.get() && currentDelta != null && ack.getSeqNo() == currentDelta.getSeqNo()) {
+ // currentDelta has been ack'ed
+ currentDelta = null;
+ if (resendTask != null) {
+ resendTask.cancel(false);
+ }
+ // reset resend counter after a successful ack to avoid inflated backoff
+ resendCount = 0;
+ if (ack.getVer() > neighborVer) {
+ // got newer neighbor's history
+ neighborVer = ack.getVer();
+ neighborLatticeIndex = to(ack.getLatticeEventsList());
+ neighborHistoryIndex = to(ack.getHistoryEventsList());
+ }
+ running.set(false);
+ // Proactively continue if:
+ // - probe success (currentNeighborVer==0), or
+ // - local inflation happened, or
+ // - neighbor's version advanced since we computed delta, or
+ // - we just sent a batch of replacements and may have more to drain
+ if (currentNeighborVer == 0
+ || lastInflationTs != currentInflationTs
+ || ack.getVer() > currentNeighborVer
+ || lastSentHasReplacement) {
+ scheduleRun();
+ }
+ // clear the flag after scheduling decision
+ lastSentHasReplacement = false;
return;
}
- // currentDelta has been ack'ed
- currentDelta = null;
- if (resendTask != null) {
- resendTask.cancel(false);
- }
+
+ // Case 2: Late or unmatched ACK. Use it to advance neighbor index if it's newer.
if (ack.getVer() > neighborVer) {
- // got newer neighbor's history
neighborVer = ack.getVer();
neighborLatticeIndex = to(ack.getLatticeEventsList());
neighborHistoryIndex = to(ack.getHistoryEventsList());
- }
- running.set(false);
- // if there are new inflation happened or probe success, restart the task
- if (currentNeighborVer == 0 || lastInflationTs != currentInflationTs) {
- scheduleRun();
+ // try schedule a run if we are not currently running
+ if (!running.get()) {
+ scheduleRun();
+ }
}
}
}
@@ -180,6 +203,7 @@ final class AntiEntropy {
.addAllHistoryEvents(to(crdtInflater.historyEvents()))
.setVer(HLC.INST.get())
.build();
+ lastSentHasReplacement = false;
send(currentDelta);
} else {
// Calculate delta
@@ -200,6 +224,7 @@ final class AntiEntropy {
.addAllHistoryEvents(to(crdtInflater.historyEvents()))
.setVer(HLC.INST.get())
.build();
+ lastSentHasReplacement = true;
send(currentDelta);
} else {
currentDelta = null;
@@ -219,7 +244,7 @@ final class AntiEntropy {
private void send(DeltaMessage deltaMessage) {
log.trace("Local[{}] send delta to neighbor[{}]:\n{}",
toPrintable(localAddr), toPrintable(neighborAddr), toPrintable(deltaMessage));
- neighborMessageSubject.onNext(new NeighborMessage(deltaMessage, neighborAddr));
+ emit(deltaMessage);
// Schedule timer task for resend
scheduleResend(deltaMessage);
}
@@ -239,9 +264,7 @@ final class AntiEntropy {
if (currentDelta == toResend) {
log.trace("Local[{}] resend delta to neighbor[{}]:\n{}",
toPrintable(localAddr), toPrintable(neighborAddr), toPrintable(toResend));
- deltaMsgCounter.increment(1D);
- deltaMsgBytesCounter.increment(currentDelta.getSerializedSize());
- neighborMessageSubject.onNext(new NeighborMessage(currentDelta, neighborAddr));
+ emit(currentDelta);
if (resendCount++ < 10) {
scheduleResend(toResend);
} else {
@@ -262,4 +285,10 @@ final class AntiEntropy {
private long resendDelay() {
return ThreadLocalRandom.current().nextLong(500, 2000) * (resendCount + 1);
}
+
+ private void emit(DeltaMessage delta) {
+ deltaMsgCounter.increment();
+ deltaMsgBytesCounter.increment(delta.getSerializedSize());
+ neighborMessageSubject.onNext(new NeighborMessage(delta, neighborAddr));
+ }
}
diff --git a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/store/AntiEntropyManager.java b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/store/AntiEntropyManager.java
index e942ddaf..2ed3cd52 100644
--- a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/store/AntiEntropyManager.java
+++ b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/store/AntiEntropyManager.java
@@ -99,7 +99,17 @@ final class AntiEntropyManager {
CompletableFuture<AckMessage> receive(DeltaMessage delta, ByteString sender) {
log.trace("Local[{}] receive delta[{}] from addr[{}]:\n{}",
toPrintable(localAddr), delta.getSeqNo(), toPrintable(sender), toPrintable(delta));
- metricManager.receiveDeltaNum.increment(1D);
+ return handleDelta(delta, sender).thenApply(ack -> {
+ metricManager.sendAckNum.increment();
+ metricManager.sendAckBytes.increment(ack.getSerializedSize());
+ log.trace("Local[{}] send ack[{}] to addr[{}]:\n{}",
+ toPrintable(localAddr), ack.getSeqNo(), toPrintable(sender), toPrintable(ack));
+ return ack;
+ });
+ }
+
+ private CompletableFuture<AckMessage> handleDelta(DeltaMessage delta, ByteString sender) {
+ metricManager.receiveDeltaNum.increment();
metricManager.receiveDeltaBytes.increment(delta.getSerializedSize());
AntiEntropy neighborAntiEntropy = neighborMap.get(sender);
if (neighborAntiEntropy != null) {
@@ -124,7 +134,7 @@ final class AntiEntropyManager {
}
void receive(AckMessage ack, ByteString neighborAddr) {
- metricManager.receiveAckNum.increment(1D);
+ metricManager.receiveAckNum.increment();
metricManager.receiveAckBytes.increment(ack.getSerializedSize());
AntiEntropy neighborAntiEntropy = neighborMap.get(neighborAddr);
if (neighborAntiEntropy != null) {
diff --git a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/store/CRDTStore.java b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/store/CRDTStore.java
index 9201d72c..7a639809 100644
--- a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/store/CRDTStore.java
+++ b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/store/CRDTStore.java
@@ -72,6 +72,7 @@ class CRDTStore implements ICRDTStore {
storeExecutor = options.storeExecutor();
String[] tags = new String[] {"store.id", storeId};
inflaterFactory = new CausalCRDTInflaterFactory(
+ options.id(),
options.inflationInterval(),
options.orHistoryExpireTime(),
options.maxCompactionTime(),
diff --git a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/util/Formatter.java b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/util/Formatter.java
index 4fc6aa7b..a7672009 100644
--- a/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/util/Formatter.java
+++ b/base-crdt/base-crdt-store/src/main/java/org/apache/bifromq/basecrdt/util/Formatter.java
@@ -38,33 +38,39 @@ public class Formatter {
}
public static Supplier<String> toPrintable(Replica replica) {
- return () -> replica.getUri() + "-" + BaseEncoding.base32().encode(replica.getId().toByteArray());
+ return () -> replica.getUri() + "-" + replica.hashCode();
}
- public static String toPrintable(DeltaMessage delta) {
- try {
- return JsonFormat.printer().print(delta);
- } catch (Exception e) {
- // ignore
- return delta.toString();
- }
+ public static Supplier<String> toPrintable(DeltaMessage delta) {
+ return () -> {
+ try {
+ return JsonFormat.printer().print(delta);
+ } catch (Exception e) {
+ // ignore
+ return delta.toString();
+ }
+ };
}
- public static String toPrintable(AckMessage ack) {
- try {
- return JsonFormat.printer().print(ack);
- } catch (Exception e) {
- // ignore
- return ack.toString();
- }
+ public static Supplier<String> toPrintable(AckMessage ack) {
+ return () -> {
+ try {
+ return JsonFormat.printer().print(ack);
+ } catch (Exception e) {
+ // ignore
+ return ack.toString();
+ }
+ };
}
- public static String toPrintable(CRDTStoreMessage ack) {
- try {
- return JsonFormat.printer().print(ack);
- } catch (Exception e) {
- // ignore
- return ack.toString();
- }
+ public static Supplier<String> toPrintable(CRDTStoreMessage ack) {
+ return () -> {
+ try {
+ return JsonFormat.printer().print(ack);
+ } catch (Exception e) {
+ // ignore
+ return ack.toString();
+ }
+ };
}
}
diff --git a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/benchmark/CRDTBenchmarkTemplate.java b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/benchmark/CRDTBenchmarkTemplate.java
index 398c4a70..6adbba88 100644
--- a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/benchmark/CRDTBenchmarkTemplate.java
+++ b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/benchmark/CRDTBenchmarkTemplate.java
@@ -14,20 +14,20 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecrdt.core.benchmark;
import static com.google.protobuf.UnsafeByteOperations.unsafeWrap;
-import org.apache.bifromq.basecrdt.core.internal.CausalCRDTInflaterFactory;
import com.google.protobuf.ByteString;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.basecrdt.core.internal.CausalCRDTInflaterFactory;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.runner.Runner;
@@ -42,7 +42,7 @@ public abstract class CRDTBenchmarkTemplate {
@Setup
public void setup() throws IOException {
- inflaterFactory = new CausalCRDTInflaterFactory(
+ inflaterFactory = new CausalCRDTInflaterFactory("testStoreId",
Duration.ofMillis(200), Duration.ofSeconds(20), Duration.ofMillis(200),
Executors.newSingleThreadScheduledExecutor());
doSetup();
diff --git a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/AWORSetTest.java b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/AWORSetTest.java
index c8e7d12e..98293ad9 100644
--- a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/AWORSetTest.java
+++ b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/AWORSetTest.java
@@ -25,12 +25,12 @@ import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
-import org.apache.bifromq.basecrdt.core.api.AWORSetOperation;
-import org.apache.bifromq.basecrdt.core.api.IAWORSet;
-import org.apache.bifromq.basecrdt.proto.Replica;
import com.google.common.collect.Sets;
import com.google.protobuf.ByteString;
import java.time.Duration;
+import org.apache.bifromq.basecrdt.core.api.AWORSetOperation;
+import org.apache.bifromq.basecrdt.core.api.IAWORSet;
+import org.apache.bifromq.basecrdt.proto.Replica;
import org.testng.annotations.Test;
public class AWORSetTest extends CRDTTest {
@@ -48,9 +48,9 @@ public class AWORSetTest extends CRDTTest {
@Test
public void testOperation() {
- AWORSetInflater aworSetInflater =
- new AWORSetInflater(leftReplica, newStateLattice(leftReplica, 1000),
- executor, Duration.ofMillis(100));
+ AWORSetInflater aworSetInflater = new AWORSetInflater("testStore", leftReplica,
+ newStateLattice(leftReplica, 1000),
+ executor, Duration.ofMillis(100));
IAWORSet aworSet = aworSetInflater.getCRDT();
assertEquals(aworSet.id(), leftReplica);
@@ -82,11 +82,11 @@ public class AWORSetTest extends CRDTTest {
@Test
public void testJoin() {
- AWORSetInflater leftInflater = new AWORSetInflater(leftReplica,
+ AWORSetInflater leftInflater = new AWORSetInflater("leftStore", leftReplica,
newStateLattice(leftReplica, 1000), executor, Duration.ofMillis(100));
IAWORSet left = leftInflater.getCRDT();
- AWORSetInflater rightInflater = new AWORSetInflater(rightReplica,
+ AWORSetInflater rightInflater = new AWORSetInflater("rightStore", rightReplica,
newStateLattice(rightReplica, 1000), executor, Duration.ofMillis(100));
IAWORSet right = rightInflater.getCRDT();
diff --git a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/CCounterTest.java b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/CCounterTest.java
index e0ffbf94..6352f320 100644
--- a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/CCounterTest.java
+++ b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/CCounterTest.java
@@ -24,14 +24,14 @@ import static org.apache.bifromq.basecrdt.core.api.CausalCRDTType.cctr;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
-import org.apache.bifromq.basecrdt.core.api.CCounterOperation;
-import org.apache.bifromq.basecrdt.core.api.ICCounter;
-import org.apache.bifromq.basecrdt.proto.Replacement;
-import org.apache.bifromq.basecrdt.proto.Replica;
import com.google.protobuf.ByteString;
import io.reactivex.rxjava3.observers.TestObserver;
import java.time.Duration;
import java.util.Optional;
+import org.apache.bifromq.basecrdt.core.api.CCounterOperation;
+import org.apache.bifromq.basecrdt.core.api.ICCounter;
+import org.apache.bifromq.basecrdt.proto.Replacement;
+import org.apache.bifromq.basecrdt.proto.Replica;
import org.testng.annotations.Test;
public class CCounterTest extends CRDTTest {
@@ -46,7 +46,7 @@ public class CCounterTest extends CRDTTest {
@Test
public void testOperation() {
- CCounterInflater cctrInflater = new CCounterInflater(leftReplica,
+ CCounterInflater cctrInflater = new CCounterInflater("leftStore", leftReplica,
newStateLattice(leftReplica, 1000), executor, Duration.ofMillis(100));
ICCounter cctr = cctrInflater.getCRDT();
assertEquals(cctr.id(), leftReplica);
@@ -73,11 +73,11 @@ public class CCounterTest extends CRDTTest {
@Test
public void testJoin() {
- CCounterInflater leftInflater = new CCounterInflater(leftReplica,
+ CCounterInflater leftInflater = new CCounterInflater("leftStore", leftReplica,
newStateLattice(leftReplica, 100000), executor, Duration.ofMillis(100));
ICCounter left = leftInflater.getCRDT();
- CCounterInflater rightInflater = new CCounterInflater(rightReplica,
+ CCounterInflater rightInflater = new CCounterInflater("rightStore", rightReplica,
newStateLattice(rightReplica, 100000), executor, Duration.ofMillis(100));
ICCounter right = rightInflater.getCRDT();
@@ -101,11 +101,11 @@ public class CCounterTest extends CRDTTest {
@Test
public void testZeroOut() {
- CCounterInflater leftInflater = new CCounterInflater(leftReplica,
+ CCounterInflater leftInflater = new CCounterInflater("leftStore", leftReplica,
newStateLattice(leftReplica, 100000), executor, Duration.ofMillis(100));
ICCounter left = leftInflater.getCRDT();
- CCounterInflater rightInflater = new CCounterInflater(rightReplica,
+ CCounterInflater rightInflater = new CCounterInflater("rightStore", rightReplica,
newStateLattice(rightReplica, 100000), executor, Duration.ofMillis(100));
ICCounter right = rightInflater.getCRDT();
@@ -129,11 +129,11 @@ public class CCounterTest extends CRDTTest {
@Test
public void testZeroOutInBatch() {
- CCounterInflater leftInflater = new CCounterInflater(leftReplica,
+ CCounterInflater leftInflater = new CCounterInflater("leftStore", leftReplica,
newStateLattice(leftReplica, 100000), executor, Duration.ofMillis(100));
ICCounter left = leftInflater.getCRDT();
- CCounterInflater rightInflater = new CCounterInflater(rightReplica,
+ CCounterInflater rightInflater = new CCounterInflater("rightStore", rightReplica,
newStateLattice(rightReplica, 100000), executor, Duration.ofMillis(100));
ICCounter right = rightInflater.getCRDT();
diff --git a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/CRDTTest.java b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/CRDTTest.java
index 1bd62318..5510283e 100644
--- a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/CRDTTest.java
+++ b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/CRDTTest.java
@@ -19,8 +19,6 @@
package org.apache.bifromq.basecrdt.core.internal;
-import org.apache.bifromq.basecrdt.proto.Replacement;
-import org.apache.bifromq.basecrdt.proto.Replica;
import com.google.common.util.concurrent.MoreExecutors;
import java.time.Duration;
import java.util.Optional;
@@ -28,6 +26,8 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
+import org.apache.bifromq.basecrdt.proto.Replacement;
+import org.apache.bifromq.basecrdt.proto.Replica;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
@@ -45,7 +45,7 @@ abstract class CRDTTest {
}
protected IReplicaStateLattice newStateLattice(Replica ownerReplica, long historyDurationInMS) {
- return new InMemReplicaStateLattice(ownerReplica,
+ return new InMemReplicaStateLattice("storeId", ownerReplica,
Duration.ofMillis(historyDurationInMS),
Duration.ofMillis(200));
}
diff --git a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/DWFlagTest.java b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/DWFlagTest.java
index 6f645d65..8a15a977 100644
--- a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/DWFlagTest.java
+++ b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/DWFlagTest.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecrdt.core.internal;
@@ -25,12 +25,12 @@ import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
-import org.apache.bifromq.basecrdt.core.api.DWFlagOperation;
-import org.apache.bifromq.basecrdt.core.api.IDWFlag;
-import org.apache.bifromq.basecrdt.proto.Replica;
import com.google.protobuf.ByteString;
import java.time.Duration;
import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.basecrdt.core.api.DWFlagOperation;
+import org.apache.bifromq.basecrdt.core.api.IDWFlag;
+import org.apache.bifromq.basecrdt.proto.Replica;
import org.testng.annotations.Test;
@Slf4j
@@ -46,7 +46,7 @@ public class DWFlagTest extends CRDTTest {
@Test
public void testOperation() {
- DWFlagInflater dwFlagInflater = new DWFlagInflater(leftReplica,
+ DWFlagInflater dwFlagInflater = new DWFlagInflater("testStore", leftReplica,
newStateLattice(leftReplica, 1000), executor, Duration.ofMillis(100));
IDWFlag dwFlag = dwFlagInflater.getCRDT();
assertEquals(dwFlag.id(), leftReplica);
@@ -65,12 +65,12 @@ public class DWFlagTest extends CRDTTest {
@Test
public void testJoin() {
- DWFlagInflater leftInflater = new DWFlagInflater(leftReplica,
+ DWFlagInflater leftInflater = new DWFlagInflater("store1", leftReplica,
newStateLattice(leftReplica, 1000000),
executor, Duration.ofMillis(100));
IDWFlag left = leftInflater.getCRDT();
- DWFlagInflater rightInflater = new DWFlagInflater(rightReplica,
+ DWFlagInflater rightInflater = new DWFlagInflater("store2", rightReplica,
newStateLattice(rightReplica, 1000000),
executor, Duration.ofMillis(100));
IDWFlag right = rightInflater.getCRDT();
diff --git a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/EWFlagTest.java b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/EWFlagTest.java
index 86bdf261..8e6f216e 100644
--- a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/EWFlagTest.java
+++ b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/EWFlagTest.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecrdt.core.internal;
@@ -25,13 +25,13 @@ import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
-import org.apache.bifromq.basecrdt.core.api.EWFlagOperation;
-import org.apache.bifromq.basecrdt.core.api.IEWFlag;
-import org.apache.bifromq.basecrdt.proto.Replica;
import com.google.protobuf.ByteString;
import io.reactivex.rxjava3.observers.TestObserver;
import java.time.Duration;
import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.basecrdt.core.api.EWFlagOperation;
+import org.apache.bifromq.basecrdt.core.api.IEWFlag;
+import org.apache.bifromq.basecrdt.proto.Replica;
import org.testng.annotations.Test;
@Slf4j
@@ -47,7 +47,7 @@ public class EWFlagTest extends CRDTTest {
@Test
public void testOperation() {
- EWFlagInflater ewFlagInflater = new EWFlagInflater(leftReplica,
+ EWFlagInflater ewFlagInflater = new EWFlagInflater("testStore", leftReplica,
newStateLattice(leftReplica, 1000),
executor, Duration.ofMillis(100));
IEWFlag ewFlag = ewFlagInflater.getCRDT();
@@ -67,11 +67,11 @@ public class EWFlagTest extends CRDTTest {
@Test
public void testJoin() {
- EWFlagInflater leftInflater = new EWFlagInflater(leftReplica,
+ EWFlagInflater leftInflater = new EWFlagInflater("lestStore", leftReplica,
newStateLattice(leftReplica, 1000000), executor, Duration.ofMillis(100));
IEWFlag left = leftInflater.getCRDT();
- EWFlagInflater rightInflater = new EWFlagInflater(rightReplica,
+ EWFlagInflater rightInflater = new EWFlagInflater("rightStore", rightReplica,
newStateLattice(rightReplica, 1000000), executor, Duration.ofMillis(100));
IEWFlag right = rightInflater.getCRDT();
diff --git a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/InMemReplicaStateLatticeTest.java b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/InMemReplicaStateLatticeTest.java
index 3ce12690..6c0c9180 100644
--- a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/InMemReplicaStateLatticeTest.java
+++ b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/InMemReplicaStateLatticeTest.java
@@ -14,46 +14,47 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecrdt.core.internal;
-import static org.apache.bifromq.basecrdt.core.internal.EventHistoryUtil.isRemembering;
-import static org.apache.bifromq.basecrdt.core.internal.ProtoUtils.dot;
-import static org.apache.bifromq.basecrdt.core.internal.ProtoUtils.replacement;
-import static org.apache.bifromq.basecrdt.core.internal.ProtoUtils.replacements;
-import static org.apache.bifromq.basecrdt.core.internal.ProtoUtils.singleDot;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Sets.newHashSet;
import static com.google.protobuf.ByteString.copyFromUtf8;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singleton;
+import static org.apache.bifromq.basecrdt.core.internal.EventHistoryUtil.isRemembering;
+import static org.apache.bifromq.basecrdt.core.internal.ProtoUtils.dot;
+import static org.apache.bifromq.basecrdt.core.internal.ProtoUtils.replacement;
+import static org.apache.bifromq.basecrdt.core.internal.ProtoUtils.replacements;
+import static org.apache.bifromq.basecrdt.core.internal.ProtoUtils.singleDot;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
-import org.apache.bifromq.basecrdt.proto.Replacement;
-import org.apache.bifromq.basecrdt.proto.Replica;
-import org.apache.bifromq.basecrdt.proto.StateLattice;
import com.google.common.collect.Sets;
import com.google.protobuf.ByteString;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import org.apache.bifromq.basecrdt.proto.Replacement;
+import org.apache.bifromq.basecrdt.proto.Replica;
+import org.apache.bifromq.basecrdt.proto.StateLattice;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class InMemReplicaStateLatticeTest {
- private InMemReplicaStateLattice testLattice;
private final Replica ownerReplica = Replica.newBuilder().setId(copyFromUtf8("Owner")).build();
private final ByteString replicaA = copyFromUtf8("A");
private final ByteString replicaB = copyFromUtf8("B");
+ private InMemReplicaStateLattice testLattice;
@BeforeMethod
public void setup() {
- testLattice = new InMemReplicaStateLattice(ownerReplica, Duration.ofMillis(1000), Duration.ofMillis(200));
+ testLattice = new InMemReplicaStateLattice("storeId", ownerReplica, Duration.ofMillis(1000),
+ Duration.ofMillis(200));
assertFalse(testLattice.lattices().hasNext());
}
@@ -495,7 +496,7 @@ public class InMemReplicaStateLatticeTest {
}
@Test
- public void compact7() throws InterruptedException {
+ public void testCompact7() throws InterruptedException {
Set<Replacement> states = newHashSet(
replacement(dot(replicaA, 4, singleDot(replicaA, 4)), dot(replicaA, 3), dot(replicaA, 1)),
replacement(dot(replicaA, 2), dot(replicaA, 1)));
diff --git a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/MVRegTest.java b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/MVRegTest.java
index 3f18b279..cb99c3ea 100644
--- a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/MVRegTest.java
+++ b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/MVRegTest.java
@@ -19,19 +19,19 @@
package org.apache.bifromq.basecrdt.core.internal;
+import static java.util.Collections.emptyIterator;
import static org.apache.bifromq.basecrdt.core.api.CRDTURI.toURI;
import static org.apache.bifromq.basecrdt.core.api.CausalCRDTType.mvreg;
-import static java.util.Collections.emptyIterator;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
-import org.apache.bifromq.basecrdt.core.api.IMVReg;
-import org.apache.bifromq.basecrdt.core.api.MVRegOperation;
-import org.apache.bifromq.basecrdt.proto.Replica;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.protobuf.ByteString;
import java.time.Duration;
+import org.apache.bifromq.basecrdt.core.api.IMVReg;
+import org.apache.bifromq.basecrdt.core.api.MVRegOperation;
+import org.apache.bifromq.basecrdt.proto.Replica;
import org.testng.annotations.Test;
public class MVRegTest extends CRDTTest {
@@ -49,7 +49,7 @@ public class MVRegTest extends CRDTTest {
@Test
public void testOperation() {
- MVRegInflater mvRegInflater = new MVRegInflater(leftReplica,
+ MVRegInflater mvRegInflater = new MVRegInflater("storeId", leftReplica,
newStateLattice(leftReplica, 1000), executor, Duration.ofMillis(100));
IMVReg mvReg = mvRegInflater.getCRDT();
assertEquals(mvReg.id(), leftReplica);
@@ -67,11 +67,11 @@ public class MVRegTest extends CRDTTest {
@Test
public void testJoin() {
- MVRegInflater leftInflater = new MVRegInflater(leftReplica, newStateLattice(leftReplica, 10000),
+ MVRegInflater leftInflater = new MVRegInflater("leftStore", leftReplica, newStateLattice(leftReplica, 10000),
executor, Duration.ofMillis(100));
IMVReg left = leftInflater.getCRDT();
- MVRegInflater rightInflater = new MVRegInflater(rightReplica, newStateLattice(rightReplica, 10000),
+ MVRegInflater rightInflater = new MVRegInflater("rightStore", rightReplica, newStateLattice(rightReplica, 10000),
executor, Duration.ofMillis(100));
IMVReg right = rightInflater.getCRDT();
@@ -96,11 +96,11 @@ public class MVRegTest extends CRDTTest {
@Test
public void testJoin1() throws InterruptedException {
- MVRegInflater leftInflater = new MVRegInflater(leftReplica, newStateLattice(leftReplica, 1000),
+ MVRegInflater leftInflater = new MVRegInflater("leftStore", leftReplica, newStateLattice(leftReplica, 1000),
executor, Duration.ofMillis(100));
IMVReg left = leftInflater.getCRDT();
- MVRegInflater rightInflater = new MVRegInflater(rightReplica, newStateLattice(rightReplica, 1000),
+ MVRegInflater rightInflater = new MVRegInflater("rightStore", rightReplica, newStateLattice(rightReplica, 1000),
executor, Duration.ofMillis(100));
IMVReg right = rightInflater.getCRDT();
diff --git a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/ORMapTest.java b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/ORMapTest.java
index 54afb219..fd5c5ecc 100644
--- a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/ORMapTest.java
+++ b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/ORMapTest.java
@@ -19,15 +19,23 @@
package org.apache.bifromq.basecrdt.core.internal;
+import static java.util.Collections.emptySet;
import static org.apache.bifromq.basecrdt.core.api.CRDTURI.toURI;
import static org.apache.bifromq.basecrdt.core.api.CausalCRDTType.mvreg;
import static org.apache.bifromq.basecrdt.core.api.CausalCRDTType.ormap;
-import static java.util.Collections.emptySet;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotEquals;
import static org.testng.Assert.assertTrue;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.protobuf.ByteString;
+import io.reactivex.rxjava3.disposables.Disposable;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import lombok.extern.slf4j.Slf4j;
import org.apache.bifromq.basecrdt.core.api.AWORSetOperation;
import org.apache.bifromq.basecrdt.core.api.CCounterOperation;
import org.apache.bifromq.basecrdt.core.api.CausalCRDTType;
@@ -44,14 +52,6 @@ import org.apache.bifromq.basecrdt.core.api.MVRegOperation;
import org.apache.bifromq.basecrdt.core.api.ORMapOperation;
import org.apache.bifromq.basecrdt.core.api.RWORSetOperation;
import org.apache.bifromq.basecrdt.proto.Replica;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
-import com.google.protobuf.ByteString;
-import io.reactivex.rxjava3.disposables.Disposable;
-import java.time.Duration;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-import lombok.extern.slf4j.Slf4j;
import org.testng.annotations.Test;
@Slf4j
@@ -76,7 +76,7 @@ public class ORMapTest extends CRDTTest {
@Test
public void testOperation() {
- ORMapInflater orMapInflater = new ORMapInflater(leftReplica, newStateLattice(leftReplica, 1000),
+ ORMapInflater orMapInflater = new ORMapInflater("storeId", leftReplica, newStateLattice(leftReplica, 1000),
executor, Duration.ofMillis(100));
IORMap ormap = orMapInflater.getCRDT();
assertEquals(ormap.id(), leftReplica);
@@ -191,11 +191,11 @@ public class ORMapTest extends CRDTTest {
@Test
public void testJoin() {
- ORMapInflater leftInflater = new ORMapInflater(leftReplica,
+ ORMapInflater leftInflater = new ORMapInflater("leftStore", leftReplica,
newStateLattice(leftReplica, 1000), executor, Duration.ofMillis(100));
IORMap leftMap = leftInflater.getCRDT();
- ORMapInflater rightInflater = new ORMapInflater(rightReplica,
+ ORMapInflater rightInflater = new ORMapInflater("rightStore", rightReplica,
newStateLattice(rightReplica, 1000), executor, Duration.ofMillis(100));
IORMap rightMap = rightInflater.getCRDT();
@@ -286,11 +286,11 @@ public class ORMapTest extends CRDTTest {
@Test
public void testJoinAfterCompaction() throws InterruptedException {
- ORMapInflater leftInflater = new ORMapInflater(leftReplica,
+ ORMapInflater leftInflater = new ORMapInflater("leftStore", leftReplica,
newStateLattice(leftReplica, 1000), executor, Duration.ofMillis(100));
IORMap leftMap = leftInflater.getCRDT();
- ORMapInflater rightInflater = new ORMapInflater(rightReplica,
+ ORMapInflater rightInflater = new ORMapInflater("rightStore", rightReplica,
newStateLattice(rightReplica, 100), executor, Duration.ofMillis(100));
IORMap rightMap = rightInflater.getCRDT();
@@ -318,7 +318,7 @@ public class ORMapTest extends CRDTTest {
@Test
public void testSubCRDTGC() {
- ORMapInflater orMapInflater = new ORMapInflater(leftReplica, newStateLattice(leftReplica, 1000),
+ ORMapInflater orMapInflater = new ORMapInflater("leftStore", leftReplica, newStateLattice(leftReplica, 1000),
executor, Duration.ofMillis(100));
IORMap orMap = orMapInflater.getCRDT();
@@ -344,7 +344,7 @@ public class ORMapTest extends CRDTTest {
@Test
public void testInflationSubscriptionWhenGC() {
- ORMapInflater orMapInflater = new ORMapInflater(leftReplica, newStateLattice(leftReplica, 1000),
+ ORMapInflater orMapInflater = new ORMapInflater("leftStore", leftReplica, newStateLattice(leftReplica, 1000),
executor, Duration.ofMillis(100));
IORMap orMap = orMapInflater.getCRDT();
AtomicInteger inflationCount = new AtomicInteger();
diff --git a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/RWORSetTest.java b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/RWORSetTest.java
index 4d332050..e677b8dd 100644
--- a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/RWORSetTest.java
+++ b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/core/internal/RWORSetTest.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basecrdt.core.internal;
@@ -25,12 +25,12 @@ import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
-import org.apache.bifromq.basecrdt.core.api.IRWORSet;
-import org.apache.bifromq.basecrdt.core.api.RWORSetOperation;
-import org.apache.bifromq.basecrdt.proto.Replica;
import com.google.common.collect.Sets;
import com.google.protobuf.ByteString;
import java.time.Duration;
+import org.apache.bifromq.basecrdt.core.api.IRWORSet;
+import org.apache.bifromq.basecrdt.core.api.RWORSetOperation;
+import org.apache.bifromq.basecrdt.proto.Replica;
import org.testng.annotations.Test;
public class RWORSetTest extends CRDTTest {
@@ -49,7 +49,7 @@ public class RWORSetTest extends CRDTTest {
@Test
public void testOperation() {
RWORSetInflater rworSetInflater =
- new RWORSetInflater(leftReplica, newStateLattice(leftReplica, 1000),
+ new RWORSetInflater("leftStore", leftReplica, newStateLattice(leftReplica, 1000),
executor, Duration.ofMillis(100));
IRWORSet rworSet = rworSetInflater.getCRDT();
assertEquals(rworSet.id(), leftReplica);
@@ -82,11 +82,11 @@ public class RWORSetTest extends CRDTTest {
@Test
public void testJoin() {
- RWORSetInflater leftInflater = new RWORSetInflater(leftReplica,
+ RWORSetInflater leftInflater = new RWORSetInflater("leftStore", leftReplica,
newStateLattice(leftReplica, 1000), executor, Duration.ofMillis(100));
IRWORSet left = leftInflater.getCRDT();
- RWORSetInflater rightInflater = new RWORSetInflater(rightReplica,
+ RWORSetInflater rightInflater = new RWORSetInflater("rightStore", rightReplica,
newStateLattice(rightReplica, 1000), executor, Duration.ofMillis(100));
IRWORSet right = rightInflater.getCRDT();
diff --git a/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/store/AntiEntropyResilienceTest.java b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/store/AntiEntropyResilienceTest.java
new file mode 100644
index 00000000..5d414a73
--- /dev/null
+++ b/base-crdt/base-crdt-store/src/test/java/org/apache/bifromq/basecrdt/store/AntiEntropyResilienceTest.java
@@ -0,0 +1,204 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bifromq.basecrdt.store;
+
+import static org.awaitility.Awaitility.await;
+
+import com.google.common.collect.Sets;
+import com.google.protobuf.ByteString;
+import io.reactivex.rxjava3.core.Observable;
+import io.reactivex.rxjava3.schedulers.Schedulers;
+import io.reactivex.rxjava3.subjects.PublishSubject;
+import io.reactivex.rxjava3.subjects.Subject;
+import java.time.Duration;
+import java.util.Collections;
+import org.apache.bifromq.basecrdt.core.api.CRDTURI;
+import org.apache.bifromq.basecrdt.core.api.CausalCRDTType;
+import org.apache.bifromq.basecrdt.core.api.IMVReg;
+import org.apache.bifromq.basecrdt.core.api.IORMap;
+import org.apache.bifromq.basecrdt.core.api.MVRegOperation;
+import org.apache.bifromq.basecrdt.core.api.ORMapOperation;
+import org.apache.bifromq.basecrdt.proto.Replica;
+import org.apache.bifromq.basecrdt.store.compressor.GzipCompressor;
+import org.apache.bifromq.basecrdt.store.proto.CRDTStoreMessage;
+import org.apache.bifromq.basecrdt.store.proto.MessagePayload;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Test;
+
+public class AntiEntropyResilienceTest {
+ private ICRDTStore storeA;
+ private ICRDTStore storeB;
+ private Subject<CRDTStoreMessage> chAB;
+ private Subject<CRDTStoreMessage> chBA;
+
+ @AfterMethod(alwaysRun = true)
+ public void teardown() {
+ if (storeA != null) {
+ storeA.stop();
+ storeA = null;
+ }
+ if (storeB != null) {
+ storeB.stop();
+ storeB = null;
+ }
+ }
+
+ @Test(groups = "integration")
+ public void testConvergeWithDroppedAckOnce() {
+ CRDTStoreOptions optsA = CRDTStoreOptions.builder()
+ .inflationInterval(Duration.ofMillis(50))
+ .maxEventsInDelta(16)
+ .build();
+ CRDTStoreOptions optsB = CRDTStoreOptions.builder()
+ .inflationInterval(Duration.ofMillis(50))
+ .maxEventsInDelta(16)
+ .build();
+ storeA = ICRDTStore.newInstance(optsA);
+ storeB = ICRDTStore.newInstance(optsB);
+
+ chAB = PublishSubject.<CRDTStoreMessage>create().toSerialized();
+ chBA = PublishSubject.<CRDTStoreMessage>create().toSerialized();
+
+ // Interpose B->A path to drop the first ACK intentionally to exercise resend/late-ack path
+ GzipCompressor compressor = new GzipCompressor();
+ final boolean[] firstAckDropped = {false};
+
+ // Start stores with the interposed channels
+ storeA.start(chBA);
+ storeB.start(chAB
+ .flatMap(msg -> {
+ // inspect payload; if it's ACK and first time, drop it once
+ MessagePayload payload = MessagePayloadUtil.decompress(compressor, msg);
+ if (payload.getMsgTypeCase() == MessagePayload.MsgTypeCase.ACK && !firstAckDropped[0]) {
+ firstAckDropped[0] = true;
+ // drop this ack
+ return Observable.empty();
+ }
+ return Observable.just(msg);
+ }));
+
+ storeA.storeMessages()
+ .observeOn(Schedulers.single())
+ .subscribe(chAB::onNext);
+ storeB.storeMessages()
+ .observeOn(Schedulers.single())
+ .subscribe(chBA::onNext);
+
+ String uri = CRDTURI.toURI(CausalCRDTType.ormap, "test");
+ // Build replicas
+ Replica rA = ReplicaIdGenerator.generate(uri);
+ Replica rB = ReplicaIdGenerator.generate(uri);
+ ByteString addrA = ByteString.copyFromUtf8("A");
+ ByteString addrB = ByteString.copyFromUtf8("B");
+
+ // Host replicas
+ IORMap ormapA = storeA.host(rA, addrA);
+ IORMap ormapB = storeB.host(rB, addrB);
+
+ // Join neighbors
+ storeA.join(rA, Collections.singleton(addrB));
+ storeB.join(rB, Collections.singleton(addrA));
+
+ // Write a value from A
+ ByteString key = ByteString.copyFromUtf8("k");
+ ByteString val = ByteString.copyFromUtf8("v1");
+ ormapA.execute(ORMapOperation.update(key).with(MVRegOperation.write(val))).join();
+
+ await().until(() -> {
+ IMVReg regB = ormapB.getMVReg(key);
+ ByteString read = Sets.newHashSet(regB.read()).stream().findFirst().orElse(ByteString.EMPTY);
+ return val.equals(read);
+ });
+ }
+
+ @Test(groups = "integration")
+ public void testConvergeWithLateUnmatchedAck() {
+ CRDTStoreOptions optsC = CRDTStoreOptions.builder()
+ .inflationInterval(Duration.ofMillis(50))
+ .maxEventsInDelta(16)
+ .build();
+ CRDTStoreOptions optsD = CRDTStoreOptions.builder()
+ .inflationInterval(Duration.ofMillis(50))
+ .maxEventsInDelta(16)
+ .build();
+ ICRDTStore storeC = ICRDTStore.newInstance(optsC);
+ ICRDTStore storeD = ICRDTStore.newInstance(optsD);
+
+ Subject<CRDTStoreMessage> cToD = PublishSubject.<CRDTStoreMessage>create().toSerialized();
+ Subject<CRDTStoreMessage> dToC = PublishSubject.<CRDTStoreMessage>create().toSerialized();
+
+ GzipCompressor compressor = new GzipCompressor();
+ final CRDTStoreMessage[] delayedAck = {null};
+ final int[] deltaCountFromC = {0};
+
+ // Wire inbound with logic: buffer first ACK from D->C, only deliver after second DELTA from C
+ storeC.start(dToC
+ .flatMap(msg -> {
+ MessagePayload payload = MessagePayloadUtil.decompress(compressor, msg);
+ if (payload.getMsgTypeCase() == MessagePayload.MsgTypeCase.ACK && delayedAck[0] == null) {
+ delayedAck[0] = msg; // buffer first ACK
+ return Observable.empty();
+ }
+ return Observable.just(msg);
+ }));
+ storeD.start(cToD
+ .flatMap(msg -> {
+ MessagePayload payload = MessagePayloadUtil.decompress(compressor, msg);
+ if (payload.getMsgTypeCase() == MessagePayload.MsgTypeCase.DELTA) {
+ deltaCountFromC[0]++;
+ if (deltaCountFromC[0] >= 2 && delayedAck[0] != null) {
+ CRDTStoreMessage ack = delayedAck[0];
+ delayedAck[0] = null;
+ dToC.onNext(ack);
+ }
+ }
+ return Observable.just(msg);
+ }));
+
+ storeC.storeMessages().observeOn(Schedulers.single()).subscribe(cToD::onNext);
+ storeD.storeMessages().observeOn(Schedulers.single()).subscribe(dToC::onNext);
+
+ // Host replicas
+ String uri = CRDTURI.toURI(CausalCRDTType.ormap, "test-late-ack");
+ Replica rC = ReplicaIdGenerator.generate(uri);
+ Replica rD = ReplicaIdGenerator.generate(uri);
+ ByteString addrC = ByteString.copyFromUtf8("C");
+ ByteString addrD = ByteString.copyFromUtf8("D");
+ IORMap ormapC = storeC.host(rC, addrC);
+ IORMap ormapD = storeD.host(rD, addrD);
+ storeC.join(rC, Collections.singleton(addrD));
+ storeD.join(rD, Collections.singleton(addrC));
+
+ // Write on C
+ ByteString key = ByteString.copyFromUtf8("k2");
+ ByteString val = ByteString.copyFromUtf8("v2");
+ ormapC.execute(ORMapOperation.update(key).with(MVRegOperation.write(val))).join();
+
+ // Await convergence on D even though first ACK is delivered late and unmatched
+ await().until(() -> {
+ IMVReg regD = ormapD.getMVReg(key);
+ ByteString read = Sets.newHashSet(regD.read()).stream().findFirst().orElse(ByteString.EMPTY);
+ return val.equals(read);
+ });
+
+ storeC.stop();
+ storeD.stop();
+ }
+}
diff --git a/base-kv/base-kv-meta-service/pom.xml b/base-kv/base-kv-meta-service/pom.xml
index 97e4c3ac..55d6f75a 100644
--- a/base-kv/base-kv-meta-service/pom.xml
+++ b/base-kv/base-kv-meta-service/pom.xml
@@ -39,6 +39,14 @@
<groupId>org.apache.bifromq</groupId>
<artifactId>base-kv-type-proto</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.bifromq</groupId>
+ <artifactId>base-logger</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.bifromq</groupId>
+ <artifactId>base-util</artifactId>
+ </dependency>
<dependency>
<groupId>io.reactivex.rxjava3</groupId>
<artifactId>rxjava</artifactId>
diff --git a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeCRDT.java b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeCRDT.java
index cc652318..0cbde7bd 100644
--- a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeCRDT.java
+++ b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeCRDT.java
@@ -37,7 +37,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
-import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.base.util.RendezvousHash;
import org.apache.bifromq.basecrdt.core.api.CausalCRDTType;
import org.apache.bifromq.basecrdt.core.api.IMVReg;
import org.apache.bifromq.basecrdt.core.api.IORMap;
@@ -47,21 +47,39 @@ import org.apache.bifromq.basecrdt.proto.Replica;
import org.apache.bifromq.basecrdt.service.ICRDTService;
import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
import org.apache.bifromq.basekv.proto.StoreKey;
+import org.apache.bifromq.logger.MDCLogger;
+import org.slf4j.Logger;
-@Slf4j
class BaseKVLandscapeCRDT implements IBaseKVLandscapeCRDT {
+ private final String clusterId;
+ private final Logger log;
private final ICRDTService crdtService;
private final IORMap landscapeORMap;
private final BehaviorSubject<Map<StoreKey, KVRangeStoreDescriptor>> landscapeSubject = BehaviorSubject.create();
private final CompositeDisposable disposable = new CompositeDisposable();
BaseKVLandscapeCRDT(String clusterId, ICRDTService crdtService) {
+ this.clusterId = clusterId;
+ this.log = MDCLogger.getLogger(BaseKVLandscapeCRDT.class, "clusterId", clusterId);
this.crdtService = crdtService;
this.landscapeORMap = crdtService.host(toLandscapeURI(clusterId));
disposable.add(landscapeORMap.inflation()
.observeOn(IBaseKVMetaService.SHARED_SCHEDULER)
.map(this::buildLandscape)
.subscribe(landscapeSubject::onNext));
+ disposable.add(Observable.combineLatest(landscape(), aliveReplicas(), (StoreDescriptorAndReplicas::new))
+ .observeOn(IBaseKVMetaService.SHARED_SCHEDULER)
+ .subscribe(this::houseKeep));
+ }
+
+ @Override
+ public String clusterId() {
+ return clusterId;
+ }
+
+ @Override
+ public Observable<Long> refreshSignal() {
+ return crdtService.refreshSignal();
}
public Observable<Set<ByteString>> aliveReplicas() {
@@ -124,4 +142,31 @@ class BaseKVLandscapeCRDT implements IBaseKVLandscapeCRDT {
l.sort((a, b) -> Long.compareUnsigned(b.getHlc(), a.getHlc()));
return Optional.ofNullable(l.isEmpty() ? null : l.get(0));
}
+
+ private void houseKeep(StoreDescriptorAndReplicas storeDescriptorAndReplicas) {
+ Map<StoreKey, KVRangeStoreDescriptor> storedDescriptors = storeDescriptorAndReplicas.descriptorMap;
+ Set<ByteString> aliveReplicas = storeDescriptorAndReplicas.replicaIds;
+ for (StoreKey storeKey : storedDescriptors.keySet()) {
+ if (!aliveReplicas.contains(storeKey.getReplicaId())
+ && shouldClean(aliveReplicas, storeKey.getReplicaId())) {
+ log.debug("store[{}] is not alive, remove its descriptor", storeKey.getStoreId());
+ removeDescriptor(storeKey);
+ }
+ }
+ }
+
+ private boolean shouldClean(Set<ByteString> aliveReplicas, ByteString failedReplicas) {
+ // Choose cleaner deterministically from the identical aliveReplicas set across nodes.
+ RendezvousHash<ByteString, ByteString> hash = RendezvousHash.<ByteString, ByteString>builder()
+ .keyFunnel((from, into) -> into.putBytes(from.asReadOnlyByteBuffer()))
+ .nodeFunnel((from, into) -> into.putBytes(from.asReadOnlyByteBuffer()))
+ .nodes(aliveReplicas)
+ .build();
+ ByteString cleaner = hash.get(failedReplicas);
+ return cleaner != null && cleaner.equals(landscapeORMap.id().getId());
+ }
+
+ private record StoreDescriptorAndReplicas(Map<StoreKey, KVRangeStoreDescriptor> descriptorMap,
+ Set<ByteString> replicaIds) {
+ }
}
diff --git a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeObserver.java b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeObserver.java
index 6f529b78..57bde6ba 100644
--- a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeObserver.java
+++ b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeObserver.java
@@ -26,13 +26,17 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
+import org.apache.bifromq.logger.MDCLogger;
+import org.slf4j.Logger;
class BaseKVLandscapeObserver implements IBaseKVLandscapeObserver {
+ private final Logger log;
private final BehaviorSubject<Map<String, KVRangeStoreDescriptor>> landscapeSubject =
BehaviorSubject.create();
private final CompositeDisposable disposable = new CompositeDisposable();
BaseKVLandscapeObserver(IBaseKVLandscapeCRDT landscapeCRDT) {
+ this.log = MDCLogger.getLogger(BaseKVLandscapeObserver.class, "clusterId", landscapeCRDT.clusterId());
disposable.add(landscapeCRDT.landscape()
.map(descriptorMap -> {
Map<String, KVRangeStoreDescriptor> descriptorMapByStoreId = new HashMap<>();
@@ -42,6 +46,7 @@ class BaseKVLandscapeObserver implements IBaseKVLandscapeObserver {
}
return v.getHlc() > value.getHlc() ? v : value;
}));
+ log.debug("Landscape changed: {}", descriptorMapByStoreId);
return descriptorMapByStoreId;
})
.subscribe(landscapeSubject::onNext));
diff --git a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeReporter.java b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeReporter.java
index 3c45e0a4..903474fe 100644
--- a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeReporter.java
+++ b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeReporter.java
@@ -19,68 +19,61 @@
package org.apache.bifromq.basekv.metaservice;
-import com.google.protobuf.ByteString;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import java.util.Map;
import java.util.Optional;
-import java.util.Set;
import java.util.concurrent.CompletableFuture;
-import lombok.extern.slf4j.Slf4j;
import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
import org.apache.bifromq.basekv.proto.StoreKey;
+import org.apache.bifromq.logger.MDCLogger;
+import org.slf4j.Logger;
-@Slf4j
class BaseKVLandscapeReporter implements IBaseKVLandscapeReporter {
+ private final Logger log;
private final String storeId;
private final IBaseKVLandscapeCRDT landscapeCRDT;
private final CompositeDisposable disposable = new CompositeDisposable();
private volatile KVRangeStoreDescriptor latestDescriptor;
BaseKVLandscapeReporter(String storeId, IBaseKVLandscapeCRDT landscapeCRDT) {
+ this.log = MDCLogger.getLogger(BaseKVLandscapeReporter.class, "clusterId", landscapeCRDT.clusterId(),
+ "storeId", storeId);
this.storeId = storeId;
this.landscapeCRDT = landscapeCRDT;
- disposable.add(Observable.combineLatest(
- landscapeCRDT.landscape(),
- landscapeCRDT.aliveReplicas(),
- (StoreDescriptorAndReplicas::new))
+ disposable.add(landscapeCRDT.landscape()
.observeOn(IBaseKVMetaService.SHARED_SCHEDULER)
- .subscribe(this::houseKeep));
+ .subscribe(this::afterInflation));
}
@Override
public CompletableFuture<Void> report(KVRangeStoreDescriptor descriptor) {
Optional<KVRangeStoreDescriptor> descriptorOnCRDT = landscapeCRDT.getStoreDescriptor(descriptor.getId());
if (descriptorOnCRDT.isEmpty() || !descriptorOnCRDT.get().equals(descriptor)) {
+ this.latestDescriptor = descriptor;
return landscapeCRDT.setStoreDescriptor(descriptor);
}
return CompletableFuture.completedFuture(null);
}
+ @Override
+ public Observable<Long> refreshSignal() {
+ return landscapeCRDT.refreshSignal();
+ }
+
@Override
public void stop() {
landscapeCRDT.removeDescriptor(storeId).join();
disposable.dispose();
}
- private void houseKeep(StoreDescriptorAndReplicas storeDescriptorAndReplicas) {
- Map<StoreKey, KVRangeStoreDescriptor> storedDescriptors = storeDescriptorAndReplicas.descriptorMap;
- Set<ByteString> aliveReplicas = storeDescriptorAndReplicas.replicaIds;
- for (StoreKey storeKey : storedDescriptors.keySet()) {
- if (!aliveReplicas.contains(storeKey.getReplicaId())) {
- log.debug("store[{}] is not alive, remove its descriptor", storeKey.getStoreId());
- landscapeCRDT.removeDescriptor(storeKey);
- }
- }
+ private void afterInflation(Map<StoreKey, KVRangeStoreDescriptor> storedDescriptors) {
if (!storedDescriptors.containsKey(landscapeCRDT.toDescriptorKey(storeId))) {
KVRangeStoreDescriptor latestDescriptor = this.latestDescriptor;
if (latestDescriptor != null) {
+ log.debug("Rectify missing store descriptor");
landscapeCRDT.setStoreDescriptor(latestDescriptor);
}
}
}
-
- private record StoreDescriptorAndReplicas(Map<StoreKey, KVRangeStoreDescriptor> descriptorMap,
- Set<ByteString> replicaIds) {
- }
}
diff --git a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesCRDT.java b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesCRDT.java
index f541c9d6..9cbec5f5 100644
--- a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesCRDT.java
+++ b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesCRDT.java
@@ -39,7 +39,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
-import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.base.util.RendezvousHash;
import org.apache.bifromq.basecrdt.core.api.CausalCRDTType;
import org.apache.bifromq.basecrdt.core.api.IMVReg;
import org.apache.bifromq.basecrdt.core.api.IORMap;
@@ -50,9 +50,12 @@ import org.apache.bifromq.basecrdt.service.ICRDTService;
import org.apache.bifromq.basehlc.HLC;
import org.apache.bifromq.basekv.proto.BalancerStateSnapshot;
import org.apache.bifromq.basekv.proto.StoreKey;
+import org.apache.bifromq.logger.MDCLogger;
+import org.slf4j.Logger;
-@Slf4j
class BaseKVStoreBalancerStatesCRDT implements IBaseKVStoreBalancerStatesCRDT {
+ private final String clusterId;
+ private final Logger log;
private final ICRDTService crdtService;
// key: storeId, value: Map of balancerClassFQN -> BalancerState
private final IORMap balancerStatesByStoreORMap;
@@ -61,12 +64,30 @@ class BaseKVStoreBalancerStatesCRDT implements IBaseKVStoreBalancerStatesCRDT {
private final CompositeDisposable disposable = new CompositeDisposable();
BaseKVStoreBalancerStatesCRDT(String clusterId, ICRDTService crdtService) {
+ this.clusterId = clusterId;
+ this.log = MDCLogger.getLogger(BaseKVStoreBalancerStatesCRDT.class, "clusterId", clusterId);
this.crdtService = crdtService;
this.balancerStatesByStoreORMap = crdtService.host(toBalancerStateURI(clusterId));
disposable.add(balancerStatesByStoreORMap.inflation()
.observeOn(IBaseKVMetaService.SHARED_SCHEDULER)
.map(this::buildBalancerStateSnapshots)
.subscribe(balancerStatesSubject::onNext));
+ disposable.add(Observable.combineLatest(
+ this.currentBalancerStates(),
+ this.aliveReplicas(),
+ (StateSnapshotsAndReplicas::new))
+ .observeOn(IBaseKVMetaService.SHARED_SCHEDULER)
+ .subscribe(this::houseKeep));
+ }
+
+ @Override
+ public String clusterId() {
+ return clusterId;
+ }
+
+ @Override
+ public Observable<Long> refuteSignal() {
+ return crdtService.refreshSignal();
}
public Observable<Set<ByteString>> aliveReplicas() {
@@ -156,4 +177,31 @@ class BaseKVStoreBalancerStatesCRDT implements IBaseKVStoreBalancerStatesCRDT {
}));
return currentBalancerStates;
}
+
+ private void houseKeep(StateSnapshotsAndReplicas stateSnapshotsAndReplicas) {
+ Map<StoreKey, Map<String, BalancerStateSnapshot>> observed = stateSnapshotsAndReplicas.observed;
+ Set<ByteString> aliveReplicas = stateSnapshotsAndReplicas.replicaIds;
+ for (StoreKey storeKey : observed.keySet()) {
+ if (!aliveReplicas.contains(storeKey.getReplicaId())
+ && shouldClean(aliveReplicas, storeKey.getReplicaId())) {
+ log.debug("store[{}] is not alive, remove its balancer states", storeKey.getStoreId());
+ this.removeStore(storeKey);
+ }
+ }
+ }
+
+ private boolean shouldClean(Set<ByteString> aliveReplicas, ByteString failedReplicas) {
+ // Choose cleaner deterministically from the identical aliveReplicas set across nodes.
+ RendezvousHash<ByteString, ByteString> hash = RendezvousHash.<ByteString, ByteString>builder()
+ .keyFunnel((from, into) -> into.putBytes(from.asReadOnlyByteBuffer()))
+ .nodeFunnel((from, into) -> into.putBytes(from.asReadOnlyByteBuffer()))
+ .nodes(aliveReplicas)
+ .build();
+ ByteString cleaner = hash.get(failedReplicas);
+ return cleaner != null && cleaner.equals(balancerStatesByStoreORMap.id().getId());
+ }
+
+ private record StateSnapshotsAndReplicas(Map<StoreKey, Map<String, BalancerStateSnapshot>> observed,
+ Set<ByteString> replicaIds) {
+ }
}
diff --git a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesObserver.java b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesObserver.java
index abe81fb1..b9fa55de 100644
--- a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesObserver.java
+++ b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesObserver.java
@@ -27,13 +27,17 @@ import io.reactivex.rxjava3.subjects.BehaviorSubject;
import java.util.HashMap;
import java.util.Map;
import org.apache.bifromq.basekv.proto.BalancerStateSnapshot;
+import org.apache.bifromq.logger.MDCLogger;
+import org.slf4j.Logger;
class BaseKVStoreBalancerStatesObserver implements IBaseKVStoreBalancerStatesObserver {
+ private final Logger log;
private final BehaviorSubject<Map<String, Map<String, BalancerStateSnapshot>>> currentBalancerStatesSubject =
BehaviorSubject.createDefault(emptyMap());
private final CompositeDisposable disposable = new CompositeDisposable();
BaseKVStoreBalancerStatesObserver(IBaseKVStoreBalancerStatesCRDT statesCRDT) {
+ this.log = MDCLogger.getLogger(BaseKVStoreBalancerStatesObserver.class, "clusterId", statesCRDT.clusterId());
disposable.add(statesCRDT.currentBalancerStates()
.observeOn(IBaseKVMetaService.SHARED_SCHEDULER)
.map(statesMap -> {
@@ -49,6 +53,7 @@ class BaseKVStoreBalancerStatesObserver implements IBaseKVStoreBalancerStatesObs
}
return balancerStates;
}));
+ log.debug("Current balancer states changed: {}", currentStates);
return currentStates;
})
.subscribe(currentBalancerStatesSubject::onNext));
diff --git a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesProposalCRDT.java b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesProposalCRDT.java
index 069b5be3..767dd7e0 100644
--- a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesProposalCRDT.java
+++ b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesProposalCRDT.java
@@ -36,7 +36,6 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
-import lombok.extern.slf4j.Slf4j;
import org.apache.bifromq.basecrdt.core.api.CausalCRDTType;
import org.apache.bifromq.basecrdt.core.api.IMVReg;
import org.apache.bifromq.basecrdt.core.api.IORMap;
@@ -45,9 +44,12 @@ import org.apache.bifromq.basecrdt.core.api.ORMapOperation;
import org.apache.bifromq.basecrdt.service.ICRDTService;
import org.apache.bifromq.basehlc.HLC;
import org.apache.bifromq.basekv.proto.BalancerStateSnapshot;
+import org.apache.bifromq.logger.MDCLogger;
+import org.slf4j.Logger;
-@Slf4j
class BaseKVStoreBalancerStatesProposalCRDT implements IBaseKVStoreBalancerStatesProposalCRDT {
+ private final String clusterId;
+ private final Logger log;
private final ICRDTService crdtService;
// key: balancerClassFQN, value: BalancerState
private final IORMap expectedBalancerStatesORMap;
@@ -56,6 +58,8 @@ class BaseKVStoreBalancerStatesProposalCRDT implements IBaseKVStoreBalancerState
private final CompositeDisposable disposable = new CompositeDisposable();
BaseKVStoreBalancerStatesProposalCRDT(String clusterId, ICRDTService crdtService) {
+ this.clusterId = clusterId;
+ this.log = MDCLogger.getLogger(BaseKVStoreBalancerStatesProposalCRDT.class, "clusterId", clusterId);
this.crdtService = crdtService;
this.expectedBalancerStatesORMap = crdtService.host(toBalancerStateProposalURI(clusterId));
disposable.add(expectedBalancerStatesORMap.inflation()
@@ -64,6 +68,11 @@ class BaseKVStoreBalancerStatesProposalCRDT implements IBaseKVStoreBalancerState
.subscribe(expectedBalancerStatesSubject::onNext));
}
+ @Override
+ public String clusterId() {
+ return clusterId;
+ }
+
public Observable<Map<String, BalancerStateSnapshot>> expectedBalancerStates() {
return expectedBalancerStatesSubject.distinctUntilChanged();
}
@@ -115,6 +124,7 @@ class BaseKVStoreBalancerStatesProposalCRDT implements IBaseKVStoreBalancerState
balancerStateOpt.ifPresent(stateSnapshot -> balancerStatesMap.put(balancerClassFQN,
stateSnapshot));
});
+ log.debug("Expected balancer states changed: {}", balancerStatesMap);
return balancerStatesMap;
}
diff --git a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesProposer.java b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesProposer.java
index 20238789..fb12538c 100644
--- a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesProposer.java
+++ b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesProposer.java
@@ -24,11 +24,15 @@ import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import org.apache.bifromq.basehlc.HLC;
import org.apache.bifromq.basekv.proto.BalancerStateSnapshot;
+import org.apache.bifromq.logger.MDCLogger;
+import org.slf4j.Logger;
class BaseKVStoreBalancerStatesProposer implements IBaseKVStoreBalancerStatesProposer {
+ private final Logger log;
private final IBaseKVStoreBalancerStatesProposalCRDT proposalCRDT;
BaseKVStoreBalancerStatesProposer(IBaseKVStoreBalancerStatesProposalCRDT proposalCRDT) {
+ this.log = MDCLogger.getLogger(BaseKVStoreBalancerStatesProposer.class, "clusterId", proposalCRDT.clusterId());
this.proposalCRDT = proposalCRDT;
}
@@ -76,6 +80,7 @@ class BaseKVStoreBalancerStatesProposer implements IBaseKVStoreBalancerStatesPro
private CompletableFuture<ProposalResult> proposeBalancerState(String balancerFactoryClass,
BalancerStateSnapshot state) {
+ log.debug("Propose balancer state: balancerClass={}, state={}", balancerFactoryClass, state);
CompletableFuture<ProposalResult> resultFuture = new CompletableFuture<>();
long now = state.getHlc();
proposalCRDT.expectedBalancerStates()
diff --git a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesReporter.java b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesReporter.java
index c4a192d0..a2531f52 100644
--- a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesReporter.java
+++ b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/BaseKVStoreBalancerStatesReporter.java
@@ -19,34 +19,32 @@
package org.apache.bifromq.basekv.metaservice;
-import com.google.protobuf.ByteString;
import com.google.protobuf.Struct;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import java.util.Map;
-import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
-import lombok.extern.slf4j.Slf4j;
import org.apache.bifromq.basekv.proto.BalancerStateSnapshot;
import org.apache.bifromq.basekv.proto.StoreKey;
+import org.apache.bifromq.logger.MDCLogger;
+import org.slf4j.Logger;
-@Slf4j
class BaseKVStoreBalancerStatesReporter implements IBaseKVStoreBalancerStatesReporter {
+ private final Logger log;
private final String storeId;
private final IBaseKVStoreBalancerStatesCRDT statesCRDT;
private final CompositeDisposable disposable = new CompositeDisposable();
private final Map<String, BalancerState> latestState = new ConcurrentHashMap<>();
BaseKVStoreBalancerStatesReporter(String storeId, IBaseKVStoreBalancerStatesCRDT statesCRDT) {
+ this.log = MDCLogger.getLogger(BaseKVStoreBalancerStatesReporter.class, "clusterId", statesCRDT.clusterId(),
+ "storeId", storeId);
this.storeId = storeId;
this.statesCRDT = statesCRDT;
- disposable.add(Observable.combineLatest(
- statesCRDT.currentBalancerStates(),
- statesCRDT.aliveReplicas(),
- (StateSnapshotsAndReplicas::new))
+ disposable.add(statesCRDT.currentBalancerStates()
.observeOn(IBaseKVMetaService.SHARED_SCHEDULER)
- .subscribe(this::houseKeep));
+ .subscribe(this::afterInflation));
}
@Override
@@ -64,32 +62,26 @@ class BaseKVStoreBalancerStatesReporter implements IBaseKVStoreBalancerStatesRep
return CompletableFuture.completedFuture(null);
}
+ @Override
+ public Observable<Long> refreshSignal() {
+ return statesCRDT.refuteSignal();
+ }
+
@Override
public void stop() {
statesCRDT.removeStore(storeId).join();
disposable.dispose();
}
- private void houseKeep(StateSnapshotsAndReplicas stateSnapshotsAndReplicas) {
- Map<StoreKey, Map<String, BalancerStateSnapshot>> observed = stateSnapshotsAndReplicas.observed;
- Set<ByteString> aliveReplicas = stateSnapshotsAndReplicas.replicaIds;
- for (StoreKey storeKey : observed.keySet()) {
- if (!aliveReplicas.contains(storeKey.getReplicaId())) {
- log.debug("store[{}] is not alive, remove its balancer states", storeKey.getStoreId());
- statesCRDT.removeStore(storeKey);
- }
- }
+ private void afterInflation(Map<StoreKey, Map<String, BalancerStateSnapshot>> observed) {
if (!observed.containsKey(statesCRDT.toDescriptorKey(storeId))) {
+ log.debug("Rectify missing store balancer states");
latestState.forEach((balancerClassFQN, balancerState) ->
statesCRDT.setStoreBalancerState(storeId, balancerClassFQN,
balancerState.enable(), balancerState.loadRules()));
}
}
- private record StateSnapshotsAndReplicas(Map<StoreKey, Map<String, BalancerStateSnapshot>> observed,
- Set<ByteString> replicaIds) {
- }
-
private record BalancerState(boolean enable, Struct loadRules) {
}
diff --git a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVLandscapeCRDT.java b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVLandscapeCRDT.java
index 1a58c7e9..4fc27eb2 100644
--- a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVLandscapeCRDT.java
+++ b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVLandscapeCRDT.java
@@ -32,6 +32,19 @@ import org.apache.bifromq.basekv.proto.StoreKey;
* The interface of a BaseKV landscape CRDT.
*/
public interface IBaseKVLandscapeCRDT {
+ /**
+ * The id of base-kv cluster.
+ * @return the cluster id
+ */
+ String clusterId();
+
+ /**
+ * A signal to refresh the landscape CRDT.
+ *
+ * @return the observable of the signal
+ */
+ Observable<Long> refreshSignal();
+
/**
* Get the observable of alive replicas of landscape CRDT.
*
diff --git a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVLandscapeReporter.java b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVLandscapeReporter.java
index 5bbdf5b7..7b39b0c9 100644
--- a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVLandscapeReporter.java
+++ b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVLandscapeReporter.java
@@ -19,6 +19,7 @@
package org.apache.bifromq.basekv.metaservice;
+import io.reactivex.rxjava3.core.Observable;
import java.util.concurrent.CompletableFuture;
import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
@@ -33,6 +34,13 @@ public interface IBaseKVLandscapeReporter {
*/
CompletableFuture<Void> report(KVRangeStoreDescriptor descriptor);
+ /**
+ * A signal to refresh the landscape reporter's state.
+ *
+ * @return an observable that emits a timestamp when the reporter should refresh its state
+ */
+ Observable<Long> refreshSignal();
+
/**
* Stop the reporter.
*/
diff --git a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVStoreBalancerStatesCRDT.java b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVStoreBalancerStatesCRDT.java
index a0e44b96..9038a307 100644
--- a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVStoreBalancerStatesCRDT.java
+++ b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVStoreBalancerStatesCRDT.java
@@ -32,6 +32,10 @@ import org.apache.bifromq.basekv.proto.StoreKey;
* The interface of a BaseKV store balancer states CRDT.
*/
public interface IBaseKVStoreBalancerStatesCRDT {
+ String clusterId();
+
+ Observable<Long> refuteSignal();
+
Observable<Set<ByteString>> aliveReplicas();
Observable<Map<StoreKey, Map<String, BalancerStateSnapshot>>> currentBalancerStates();
diff --git a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVStoreBalancerStatesProposalCRDT.java b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVStoreBalancerStatesProposalCRDT.java
index 3ca2d36e..4bde0de7 100644
--- a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVStoreBalancerStatesProposalCRDT.java
+++ b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVStoreBalancerStatesProposalCRDT.java
@@ -30,6 +30,7 @@ import org.apache.bifromq.basekv.proto.BalancerStateSnapshot;
* The interface of a BaseKV store balancer states CRDT.
*/
public interface IBaseKVStoreBalancerStatesProposalCRDT {
+ String clusterId();
Observable<Map<String, BalancerStateSnapshot>> expectedBalancerStates();
diff --git a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVStoreBalancerStatesReporter.java b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVStoreBalancerStatesReporter.java
index c4ca8267..9e6891f1 100644
--- a/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVStoreBalancerStatesReporter.java
+++ b/base-kv/base-kv-meta-service/src/main/java/org/apache/bifromq/basekv/metaservice/IBaseKVStoreBalancerStatesReporter.java
@@ -20,6 +20,7 @@
package org.apache.bifromq.basekv.metaservice;
import com.google.protobuf.Struct;
+import io.reactivex.rxjava3.core.Observable;
import java.util.concurrent.CompletableFuture;
/**
@@ -36,6 +37,13 @@ public interface IBaseKVStoreBalancerStatesReporter {
*/
CompletableFuture<Void> reportBalancerState(String balancerFactoryClassFQN, boolean disable, Struct loadRules);
+ /**
+ * A signal to refresh the reporter's state.
+ *
+ * @return an observable that emits a timestamp when the reporter should refresh its state
+ */
+ Observable<Long> refreshSignal();
+
/**
* Stop the reporter.
*/
diff --git a/base-kv/base-kv-meta-service/src/test/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeReportTest.java b/base-kv/base-kv-meta-service/src/test/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeReportTest.java
index f28fbea4..87aaf741 100644
--- a/base-kv/base-kv-meta-service/src/test/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeReportTest.java
+++ b/base-kv/base-kv-meta-service/src/test/java/org/apache/bifromq/basekv/metaservice/BaseKVLandscapeReportTest.java
@@ -21,9 +21,7 @@ package org.apache.bifromq.basekv.metaservice;
import static org.awaitility.Awaitility.await;
import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertTrue;
-import java.util.Collections;
import java.util.Map;
import org.apache.bifromq.basecluster.AgentHostOptions;
import org.apache.bifromq.basecluster.IAgentHost;
@@ -80,7 +78,6 @@ public class BaseKVLandscapeReportTest {
await().until(() -> observer.getStoreDescriptor(descriptor.getId()).isPresent());
reporter.stop();
- assertEquals(Collections.emptyMap(), observer.landscape().blockingFirst());
- assertTrue(observer.getStoreDescriptor(descriptor.getId()).isEmpty());
+ await().until(() -> observer.landscape().blockingFirst().isEmpty());
}
}
diff --git a/base-kv/base-kv-raft/src/main/java/org/apache/bifromq/basekv/raft/RaftNodeStateFollower.java b/base-kv/base-kv-raft/src/main/java/org/apache/bifromq/basekv/raft/RaftNodeStateFollower.java
index 62bf6666..c488260e 100644
--- a/base-kv/base-kv-raft/src/main/java/org/apache/bifromq/basekv/raft/RaftNodeStateFollower.java
+++ b/base-kv/base-kv-raft/src/main/java/org/apache/bifromq/basekv/raft/RaftNodeStateFollower.java
@@ -14,11 +14,21 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basekv.raft;
+import com.google.protobuf.ByteString;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.CompletableFuture;
import org.apache.bifromq.basekv.raft.exception.ClusterConfigChangeException;
import org.apache.bifromq.basekv.raft.exception.DropProposalException;
import org.apache.bifromq.basekv.raft.exception.LeaderTransferException;
@@ -39,16 +49,6 @@ import org.apache.bifromq.basekv.raft.proto.RequestReadIndexReply;
import org.apache.bifromq.basekv.raft.proto.RequestVote;
import org.apache.bifromq.basekv.raft.proto.Snapshot;
import org.apache.bifromq.basekv.raft.proto.Voting;
-import com.google.protobuf.ByteString;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.concurrent.CompletableFuture;
class RaftNodeStateFollower extends RaftNodeState {
private final TreeMap<Long, StabilizingTask> stabilizingIndexes = new TreeMap<>(Long::compareTo);
@@ -387,7 +387,7 @@ class RaftNodeStateFollower extends RaftNodeState {
void onSnapshotRestored(ByteString requested, ByteString installed, Throwable ex, CompletableFuture<Void> onDone) {
if (currentISSRequest == null) {
log.debug("Snapshot installation request not found");
- onDone.completeExceptionally(new SnapshotException("No snapshot installation request"));
+ onDone.completeExceptionally(SnapshotException.noSnapshot());
return;
}
InstallSnapshot iss = currentISSRequest;
@@ -398,7 +398,7 @@ class RaftNodeStateFollower extends RaftNodeState {
onDone.completeExceptionally(ex);
} else {
log.debug("Obsolete snapshot installation");
- onDone.completeExceptionally(new SnapshotException("Obsolete snapshot installed by FSM"));
+ onDone.completeExceptionally(SnapshotException.obsolete());
}
return;
}
diff --git a/base-kv/base-kv-raft/src/main/java/org/apache/bifromq/basekv/raft/exception/SnapshotException.java b/base-kv/base-kv-raft/src/main/java/org/apache/bifromq/basekv/raft/exception/SnapshotException.java
index ec397ed2..3f4bdf66 100644
--- a/base-kv/base-kv-raft/src/main/java/org/apache/bifromq/basekv/raft/exception/SnapshotException.java
+++ b/base-kv/base-kv-raft/src/main/java/org/apache/bifromq/basekv/raft/exception/SnapshotException.java
@@ -14,17 +14,47 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basekv.raft.exception;
+/**
+ * Exception thrown during snapshot operations in the Raft protocol.
+ * This exception can indicate that a snapshot is obsolete or has other issues.
+ */
public class SnapshotException extends RuntimeException {
- public SnapshotException(String message) {
+ private SnapshotException(String message) {
super(message);
}
- public SnapshotException(Throwable e) {
+ private SnapshotException(Throwable e) {
super(e);
}
+
+ public static ObsoleteSnapshotException obsolete() {
+ return new ObsoleteSnapshotException();
+ }
+
+ public static NoSnapshotException noSnapshot() {
+ return new NoSnapshotException();
+ }
+
+ /**
+ * Exception indicating that no snapshot is available for installation.
+ */
+ public static class NoSnapshotException extends SnapshotException {
+ private NoSnapshotException() {
+ super("No snapshot available");
+ }
+ }
+
+ /**
+ * Exception indicating that the snapshot is obsolete by a newer snapshot during installation.
+ */
+ public static class ObsoleteSnapshotException extends SnapshotException {
+ private ObsoleteSnapshotException() {
+ super("The installed snapshot has been obsoleted by a newer snapshot");
+ }
+ }
}
diff --git a/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/KVStoreBalanceController.java b/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/KVStoreBalanceController.java
index 5e511d11..5f73f98e 100644
--- a/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/KVStoreBalanceController.java
+++ b/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/KVStoreBalanceController.java
@@ -31,6 +31,7 @@ import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
@@ -120,7 +121,7 @@ public class KVStoreBalanceController {
this.customBalancerFactories = Lists.newArrayList(factories);
this.builtinBalancerFactories = Lists.newArrayList(
new RangeBootstrapBalancerFactory(bootstrapDelay),
- new RedundantRangeRemovalBalancerFactory(),
+ new RedundantRangeRemovalBalancerFactory(zombieProbeDelay),
new UnreachableReplicaRemovalBalancerFactory(zombieProbeDelay));
this.statesProposal = metaService.balancerStatesProposal(storeClient.clusterId());
this.balancers = new HashMap<>();
@@ -137,7 +138,8 @@ public class KVStoreBalanceController {
if (state.compareAndSet(State.Init, State.Started)) {
this.localStoreId = localStoreId;
statesReporter = metaService.balancerStatesReporter(storeClient.clusterId(), localStoreId);
- log = MDCLogger.getLogger("balancer.logger", "clusterId", storeClient.clusterId(), "storeId", localStoreId);
+ log = MDCLogger.getLogger("balancer.logger",
+ "clusterId", storeClient.clusterId(), "storeId", localStoreId, "balancer", "CONTROLLER");
for (IStoreBalancerFactory factory : builtinBalancerFactories) {
StoreBalancer balancer = factory.newBalancer(storeClient.clusterId(), localStoreId);
@@ -146,12 +148,12 @@ public class KVStoreBalanceController {
}
for (IStoreBalancerFactory factory : customBalancerFactories) {
String balancerFactoryFQN = factory.getClass().getName();
- log.info("Create balancer from factory: {}", balancerFactoryFQN);
StoreBalancer balancer = factory.newBalancer(storeClient.clusterId(), localStoreId);
+ log.info("Create balancer[{}] from factory: {}", balancer.getClass().getName(), balancerFactoryFQN);
if (balancer instanceof RangeBootstrapBalancer
|| balancer instanceof RedundantRangeRemovalBalancer
|| balancer instanceof UnreachableReplicaRemovalBalancer) {
- log.warn("{} should not be created from custom balancer factory",
+ log.warn("Builtin balancer[{}] should not be created from custom balancer factory",
balancer.getClass().getSimpleName());
continue;
}
@@ -163,14 +165,28 @@ public class KVStoreBalanceController {
log.info("BalancerController start");
disposables.add(statesProposal.expectedBalancerStates()
.subscribe(currentExpected -> {
+ log.trace("Expected balancer states changed: {}", currentExpected);
this.expectedBalancerStates = currentExpected;
trigger();
}));
disposables.add(storeClient.describe().subscribe(descriptors -> {
+ log.trace("Landscape changed: {}", descriptors);
this.landscape = descriptors;
trimRangeHistory(descriptors);
trigger();
}));
+ disposables.add(statesReporter.refreshSignal()
+ .subscribe(ts -> {
+ for (Map.Entry<String, StoreBalancerState> entry : balancers.entrySet()) {
+ String balancerFacClassFQN = entry.getKey();
+ StoreBalancerState balancerState = entry.getValue();
+ if (!balancerState.isBuiltin) {
+ log.debug("Report balancer state for {}", balancerFacClassFQN);
+ statesReporter.reportBalancerState(balancerFacClassFQN,
+ balancerState.disabled.get(), balancerState.loadRules.get());
+ }
+ }
+ }));
}
}
@@ -199,6 +215,10 @@ public class KVStoreBalanceController {
private void trigger() {
if (state.get() == State.Started && scheduling.compareAndSet(false, true)) {
long jitter = ThreadLocalRandom.current().nextLong(0, retryDelay.toMillis());
+ if (task != null && !task.isDone()) {
+ log.trace("Cancel scheduled balance task");
+ task.cancel(true);
+ }
task = executor.schedule(this::updateAndBalance, jitter, TimeUnit.MILLISECONDS);
}
}
@@ -208,6 +228,9 @@ public class KVStoreBalanceController {
Set<KVRangeStoreDescriptor> landscape = this.landscape;
if (landscape == null || landscape.isEmpty()) {
scheduling.set(false);
+ if (!Objects.equals(this.landscape, landscape)) {
+ trigger();
+ }
return;
}
for (Map.Entry<String, StoreBalancerState> entry : balancers.entrySet()) {
@@ -222,18 +245,25 @@ public class KVStoreBalanceController {
Struct loadRules = balancerState.loadRules.get();
boolean needReport = false;
if (balancerState.disabled.get() != disable) {
- log.info("Balancer[{}] is {}", balancerFacClassFQN, disable ? "disabled" : "enabled");
+ log.info("Balancer[{}] is {}", balancerState.balancer.getClass().getSimpleName(),
+ disable ? "disabled" : "enabled");
balancerState.disabled.set(disable);
needReport = true;
}
- Struct expectedLoadRules = expectedState.getLoadRules();
- if (!loadRules.equals(expectedLoadRules)
- && balancerState.balancer.validate(expectedLoadRules)) {
- loadRules = expectedLoadRules;
- // report the balancer state
- balancerState.loadRules.set(expectedLoadRules);
- balancerState.balancer.update(expectedLoadRules);
- needReport = true;
+ Struct expectedLoadRules = loadRules.toBuilder()
+ .mergeFrom(expectedState.getLoadRules())
+ .build();
+ if (!loadRules.equals(expectedLoadRules)) {
+ if (balancerState.balancer.validate(expectedLoadRules)) {
+ loadRules = expectedLoadRules;
+ // report the balancer state
+ balancerState.loadRules.set(expectedLoadRules);
+ balancerState.balancer.update(expectedLoadRules);
+ needReport = true;
+ } else {
+ log.warn("Balancer[{}] load rules not valid: {}",
+ balancerState.balancer.getClass().getSimpleName(), expectedLoadRules);
+ }
}
if (needReport) {
statesReporter.reportBalancerState(balancerFacClassFQN, disable, loadRules);
@@ -245,7 +275,7 @@ public class KVStoreBalanceController {
}
balancerState.balancer.update(landscape);
} catch (Throwable e) {
- log.error("Balancer[{}] update failed", balancerFacClassFQN, e);
+ log.error("Balancer[{}] update failed", balancerState.balancer.getClass().getSimpleName(), e);
}
}
balance(expectedBalancerState, landscape);
@@ -254,9 +284,11 @@ public class KVStoreBalanceController {
private void scheduleRetry(Map<String, BalancerStateSnapshot> expected,
Set<KVRangeStoreDescriptor> landscape,
Duration delay) {
+ log.debug("Retry balance after {}s", delay.toSeconds());
task = executor.schedule(() -> {
- if (expected != this.expectedBalancerStates || landscape != this.landscape) {
+ if (!Objects.equals(expected, this.expectedBalancerStates) || landscape != this.landscape) {
// retry is preemptive
+ log.trace("Balance retry is preempted");
return;
}
if (scheduling.compareAndSet(false, true)) {
@@ -268,11 +300,11 @@ public class KVStoreBalanceController {
private void balance(final Map<String, BalancerStateSnapshot> expected,
final Set<KVRangeStoreDescriptor> landscape) {
metricsManager.scheduleCount.increment();
- Duration delay = Duration.ZERO;
+ Duration delay = null;
for (Map.Entry<String, StoreBalancerState> entry : balancers.entrySet()) {
- String balancerFactoryName = entry.getKey();
StoreBalancerState fromBalancerState = entry.getValue();
StoreBalancer fromBalancer = fromBalancerState.balancer;
+ String balancerName = fromBalancer.getClass().getSimpleName();
if (fromBalancerState.disabled.get()) {
continue;
}
@@ -282,9 +314,8 @@ public class KVStoreBalanceController {
case BalanceNow -> {
BalanceCommand commandToRun = ((BalanceNow<?>) result).command;
if (!isStaleCommand(commandToRun)) {
- log.info("Balancer[{}] command run: {}", balancerFactoryName, commandToRun);
- String balancerName = fromBalancer.getClass().getSimpleName();
String cmdName = commandToRun.getClass().getSimpleName();
+ log.info("Balancer[{}] command run: {}", balancerName, commandToRun);
Sample start = Timer.start();
runCommand(commandToRun)
.whenCompleteAsync((success, e) -> {
@@ -295,7 +326,7 @@ public class KVStoreBalanceController {
metrics.cmdFailedCounter.increment();
} else {
log.info("Balancer[{}] command run result[{}]: {}",
- balancerFactoryName, success, commandToRun);
+ balancerName, success, commandToRun);
if (success) {
metrics.cmdSucceedCounter.increment();
start.stop(metrics.cmdRunTimer);
@@ -305,7 +336,8 @@ public class KVStoreBalanceController {
}
scheduling.set(false);
if (success) {
- if (this.landscape != landscape || this.expectedBalancerStates != expected) {
+ if (!Objects.equals(this.landscape, landscape)
+ || !Objects.equals(this.expectedBalancerStates, expected)) {
trigger();
}
} else {
@@ -317,21 +349,21 @@ public class KVStoreBalanceController {
}
case AwaitBalance -> {
Duration await = ((AwaitBalance) result).await;
- delay = await.toNanos() > delay.toNanos() ? await : delay;
+ delay = delay != null ? (await.toNanos() < delay.toNanos() ? await : delay) : await;
}
default -> {
// do nothing
}
}
} catch (Throwable e) {
- log.warn("Balancer[{}] unexpected error", balancerFactoryName, e);
+ log.warn("Balancer[{}] unexpected error", balancerName, e);
}
}
// no command to run
scheduling.set(false);
- if (this.landscape != landscape || this.expectedBalancerStates != expected) {
+ if (!Objects.equals(this.landscape, landscape) || !Objects.equals(this.expectedBalancerStates, expected)) {
trigger();
- } else if (!delay.isZero()) {
+ } else if (delay != null) {
// if some balancers are in the progress of generating balance command, wait for a while
scheduleRetry(expected, landscape, delay);
}
diff --git a/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/RedundantRangeRemovalBalancerFactory.java b/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/RedundantRangeRemovalBalancerFactory.java
index 4eff0bed..3bcf0197 100644
--- a/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/RedundantRangeRemovalBalancerFactory.java
+++ b/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/RedundantRangeRemovalBalancerFactory.java
@@ -19,14 +19,22 @@
package org.apache.bifromq.basekv.balance;
+import java.time.Duration;
+import org.apache.bifromq.basehlc.HLC;
import org.apache.bifromq.basekv.balance.impl.RedundantRangeRemovalBalancer;
/**
* Builtin balancer for redundant range removal.
*/
class RedundantRangeRemovalBalancerFactory implements IStoreBalancerFactory {
+ private final Duration delay;
+
+ RedundantRangeRemovalBalancerFactory(Duration delay) {
+ this.delay = delay;
+ }
+
@Override
public StoreBalancer newBalancer(String clusterId, String localStoreId) {
- return new RedundantRangeRemovalBalancer(clusterId, localStoreId);
+ return new RedundantRangeRemovalBalancer(clusterId, localStoreId, delay, HLC.INST::getPhysical);
}
}
diff --git a/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/RangeBootstrapBalancer.java b/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/RangeBootstrapBalancer.java
index 38b7eebe..c9d979d3 100644
--- a/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/RangeBootstrapBalancer.java
+++ b/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/RangeBootstrapBalancer.java
@@ -22,7 +22,14 @@ package org.apache.bifromq.basekv.balance.impl;
import static org.apache.bifromq.basekv.utils.BoundaryUtil.FULL_BOUNDARY;
import static org.apache.bifromq.basekv.utils.DescriptorUtil.getEffectiveEpoch;
+import java.time.Duration;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
import org.apache.bifromq.basehlc.HLC;
+import org.apache.bifromq.basekv.balance.AwaitBalance;
import org.apache.bifromq.basekv.balance.BalanceNow;
import org.apache.bifromq.basekv.balance.BalanceResult;
import org.apache.bifromq.basekv.balance.NoNeedBalance;
@@ -33,12 +40,6 @@ import org.apache.bifromq.basekv.proto.KVRangeId;
import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
import org.apache.bifromq.basekv.utils.EffectiveEpoch;
import org.apache.bifromq.basekv.utils.KVRangeIdUtil;
-import java.time.Duration;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.ThreadLocalRandom;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Supplier;
/**
* RangeBootstrapBalancer is a specialized StoreBalancer designed to handle the bootstrap process of creating the
@@ -49,6 +50,7 @@ public class RangeBootstrapBalancer extends StoreBalancer {
private final Supplier<Long> millisSource;
private final long suspicionDurationMillis;
private final AtomicReference<BootstrapTrigger> bootstrapTrigger = new AtomicReference<>();
+
/**
* Constructor of StoreBalancer.
*
@@ -98,19 +100,27 @@ public class RangeBootstrapBalancer extends StoreBalancer {
KVRangeIdUtil.toString(rangeId));
bootstrapTrigger.set(new BootstrapTrigger(rangeId, FULL_BOUNDARY, randomSuspicionTimeout()));
}
+ } else if (bootstrapTrigger.get() != null) {
+ log.debug("Effective epoch found: {}, cancel any pending bootstrap", effectiveEpoch.get().epoch());
+ bootstrapTrigger.set(null);
}
}
@Override
public BalanceResult balance() {
BootstrapTrigger current = bootstrapTrigger.get();
- if (current != null && millisSource.get() > current.triggerTime) {
- bootstrapTrigger.set(null);
- return BalanceNow.of(BootstrapCommand.builder()
- .toStore(localStoreId)
- .kvRangeId(current.id)
- .boundary(current.boundary)
- .build());
+ if (current != null) {
+ long nowMillis = millisSource.get();
+ if (nowMillis > current.triggerTime) {
+ bootstrapTrigger.set(null);
+ return BalanceNow.of(BootstrapCommand.builder()
+ .toStore(localStoreId)
+ .kvRangeId(current.id)
+ .boundary(current.boundary)
+ .build());
+ } else {
+ return AwaitBalance.of(Duration.ofMillis(current.triggerTime - nowMillis));
+ }
}
return NoNeedBalance.INSTANCE;
}
diff --git a/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/RangeSplitBalancer.java b/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/RangeSplitBalancer.java
index b9642fe4..7089ef3c 100644
--- a/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/RangeSplitBalancer.java
+++ b/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/RangeSplitBalancer.java
@@ -26,9 +26,12 @@ import static org.apache.bifromq.basekv.utils.BoundaryUtil.startKey;
import com.google.protobuf.Struct;
import com.google.protobuf.Value;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import org.apache.bifromq.basekv.proto.Boundary;
import org.apache.bifromq.basekv.proto.KVRangeDescriptor;
import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
@@ -131,6 +134,10 @@ public class RangeSplitBalancer extends RuleBasedPlacementBalancer {
KVRangeDescriptor rangeDescriptor = leaderRange.descriptor();
KVRangeStoreDescriptor storeDescriptor = landscape.get(leaderRange.ownerStoreDescriptor().getId());
ClusterConfig clusterConfig = rangeDescriptor.getConfig();
+ if (containsDeadMember(clusterConfig, landscape.keySet())) {
+ // shortcut when config contains dead members
+ return Collections.emptyMap();
+ }
Optional<SplitHint> splitHintOpt = rangeDescriptor
.getHintsList()
.stream()
@@ -170,4 +177,13 @@ public class RangeSplitBalancer extends RuleBasedPlacementBalancer {
}
return expectedRangeLayout;
}
+
+ private boolean containsDeadMember(ClusterConfig clusterConfig, Set<String> live) {
+ Set<String> members = new HashSet<>();
+ members.addAll(clusterConfig.getVotersList());
+ members.addAll(clusterConfig.getLearnersList());
+ members.addAll(clusterConfig.getNextVotersList());
+ members.addAll(clusterConfig.getNextLearnersList());
+ return members.stream().anyMatch(m -> !live.contains(m));
+ }
}
diff --git a/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/RedundantRangeRemovalBalancer.java b/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/RedundantRangeRemovalBalancer.java
index bedf16fb..0ae68e33 100644
--- a/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/RedundantRangeRemovalBalancer.java
+++ b/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/RedundantRangeRemovalBalancer.java
@@ -23,21 +23,31 @@ import static org.apache.bifromq.basekv.balance.util.CommandUtil.quit;
import static org.apache.bifromq.basekv.utils.DescriptorUtil.getEffectiveRoute;
import static org.apache.bifromq.basekv.utils.DescriptorUtil.organizeByEpoch;
+import com.google.common.collect.Sets;
+import java.time.Duration;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.NavigableMap;
+import java.util.NavigableSet;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import org.apache.bifromq.basekv.balance.AwaitBalance;
+import org.apache.bifromq.basekv.balance.BalanceNow;
import org.apache.bifromq.basekv.balance.BalanceResult;
import org.apache.bifromq.basekv.balance.NoNeedBalance;
import org.apache.bifromq.basekv.balance.StoreBalancer;
+import org.apache.bifromq.basekv.balance.command.BalanceCommand;
import org.apache.bifromq.basekv.proto.Boundary;
import org.apache.bifromq.basekv.proto.KVRangeDescriptor;
import org.apache.bifromq.basekv.proto.KVRangeId;
import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
+import org.apache.bifromq.basekv.raft.proto.ClusterConfig;
import org.apache.bifromq.basekv.raft.proto.RaftNodeStatus;
import org.apache.bifromq.basekv.utils.EffectiveEpoch;
import org.apache.bifromq.basekv.utils.KVRangeIdUtil;
@@ -55,7 +65,9 @@ import org.apache.bifromq.basekv.utils.LeaderRange;
* caution.</p>
*/
public class RedundantRangeRemovalBalancer extends StoreBalancer {
- private volatile NavigableMap<Long, Set<KVRangeStoreDescriptor>> latest = Collections.emptyNavigableMap();
+ private final Supplier<Long> millisSource;
+ private final long suspicionDurationMillis;
+ private final AtomicReference<PendingQuitCommand> pendingQuitCommand = new AtomicReference<>();
/**
* Constructor of StoreBalancer.
@@ -63,23 +75,60 @@ public class RedundantRangeRemovalBalancer extends StoreBalancer {
* @param clusterId the id of the BaseKV cluster which the store belongs to
* @param localStoreId the id of the store which the balancer is responsible for
*/
- public RedundantRangeRemovalBalancer(String clusterId, String localStoreId) {
+ public RedundantRangeRemovalBalancer(String clusterId,
+ String localStoreId,
+ Duration suspicionDuration,
+ Supplier<Long> millisSource) {
super(clusterId, localStoreId);
+ this.suspicionDurationMillis = suspicionDuration.toMillis();
+ this.millisSource = millisSource;
}
@Override
public void update(Set<KVRangeStoreDescriptor> landscape) {
- latest = organizeByEpoch(landscape);
+ NavigableMap<Long, Set<KVRangeStoreDescriptor>> landscapeByEpoch = organizeByEpoch(landscape);
+ if (landscapeByEpoch.isEmpty()) {
+ pendingQuitCommand.set(null);
+ return;
+ }
+ boolean scheduled = cleanupRedundantEpoch(landscapeByEpoch);
+ if (scheduled) {
+ return;
+ }
+ Map.Entry<Long, Set<KVRangeStoreDescriptor>> oldestEntry = landscapeByEpoch.firstEntry();
+ EffectiveEpoch effectiveEpoch = new EffectiveEpoch(oldestEntry.getKey(), oldestEntry.getValue());
+ scheduled = cleanupIdConflictRange(effectiveEpoch);
+ if (scheduled) {
+ return;
+ }
+ scheduled = cleanupBoundaryConflictRange(effectiveEpoch);
+ if (!scheduled) {
+ if (pendingQuitCommand.get() != null) {
+ log.debug("No redundant range found, clear pending quit command");
+ pendingQuitCommand.set(null);
+ }
+ }
}
@Override
public BalanceResult balance() {
- if (latest.isEmpty()) {
- return NoNeedBalance.INSTANCE;
+ PendingQuitCommand current = pendingQuitCommand.get();
+ if (current != null) {
+ long nowMillis = millisSource.get();
+ if (nowMillis > current.triggerTime) {
+ pendingQuitCommand.set(null);
+ return BalanceNow.of(current.quitCmd);
+ } else {
+ return AwaitBalance.of(Duration.ofMillis(current.triggerTime - nowMillis));
+ }
}
- if (latest.size() > 1) {
+ return NoNeedBalance.INSTANCE;
+ }
+
+ private boolean cleanupRedundantEpoch(NavigableMap<Long, Set<KVRangeStoreDescriptor>> landscapeByEpoch) {
+ if (landscapeByEpoch.size() > 1) {
// deal with epoch-conflict ranges
- Set<KVRangeStoreDescriptor> storeDescriptors = latest.lastEntry().getValue();
+ Set<KVRangeStoreDescriptor> storeDescriptors = landscapeByEpoch.lastEntry().getValue();
for (KVRangeStoreDescriptor storeDescriptor : storeDescriptors) {
if (!storeDescriptor.getId().equals(localStoreId)) {
continue;
@@ -88,34 +137,42 @@ public class RedundantRangeRemovalBalancer extends StoreBalancer {
if (rangeDescriptor.getRole() != RaftNodeStatus.Leader) {
continue;
}
- log.debug("Remove Epoch-Conflict range: {} in store {}",
- KVRangeIdUtil.toString(rangeDescriptor.getId()),
- storeDescriptor.getId());
- return quit(localStoreId, rangeDescriptor);
+ log.debug("Schedule command to remove epoch-conflict range: id={}, boundary={}",
+ KVRangeIdUtil.toString(rangeDescriptor.getId()), rangeDescriptor.getBoundary());
+ pendingQuitCommand.set(
+ new PendingQuitCommand(quit(localStoreId, rangeDescriptor), randomSuspicionTimeout()));
+ return true;
}
}
- return NoNeedBalance.INSTANCE;
}
- Map.Entry<Long, Set<KVRangeStoreDescriptor>> oldestEntry = latest.firstEntry();
- Map<KVRangeId, SortedSet<LeaderRange>> conflictingRanges = findConflictingRanges(oldestEntry.getValue());
+ return false;
+ }
+
+ private boolean cleanupIdConflictRange(EffectiveEpoch effectiveEpoch) {
+ Map<KVRangeId, NavigableSet<LeaderRange>> conflictingRanges =
+ findConflictingRanges(effectiveEpoch.storeDescriptors());
if (!conflictingRanges.isEmpty()) {
// deal with id-conflict ranges
for (KVRangeId rangeId : conflictingRanges.keySet()) {
- SortedSet<LeaderRange> leaderRanges = conflictingRanges.get(rangeId);
+ NavigableSet<LeaderRange> leaderRanges = conflictingRanges.get(rangeId);
for (LeaderRange leaderRange : leaderRanges) {
if (!leaderRange.ownerStoreDescriptor().getId().equals(localStoreId)) {
- return NoNeedBalance.INSTANCE;
+ return false;
}
- log.debug("Remove Id-Conflict range: {} in store {}",
+ log.warn("Schedule command to remove id-conflict range: id={}, boundary={}",
KVRangeIdUtil.toString(leaderRange.descriptor().getId()),
- leaderRange.ownerStoreDescriptor().getId());
- return quit(localStoreId, leaderRange.descriptor());
+ leaderRange.descriptor().getBoundary());
+ pendingQuitCommand.set(
+ new PendingQuitCommand(quit(localStoreId, leaderRange.descriptor()), randomSuspicionTimeout()));
+ return true;
}
}
- return NoNeedBalance.INSTANCE;
}
+ return false;
+ }
+
+ private boolean cleanupBoundaryConflictRange(EffectiveEpoch effectiveEpoch) {
// deal with boundary-conflict ranges
- EffectiveEpoch effectiveEpoch = new EffectiveEpoch(oldestEntry.getKey(), oldestEntry.getValue());
NavigableMap<Boundary, LeaderRange> effectiveLeaders = getEffectiveRoute(effectiveEpoch).leaderRanges();
for (KVRangeStoreDescriptor storeDescriptor : effectiveEpoch.storeDescriptors()) {
if (!storeDescriptor.getId().equals(localStoreId)) {
@@ -128,19 +185,21 @@ public class RedundantRangeRemovalBalancer extends StoreBalancer {
Boundary boundary = rangeDescriptor.getBoundary();
LeaderRange leaderRange = effectiveLeaders.get(boundary);
if (leaderRange == null || !leaderRange.descriptor().getId().equals(rangeDescriptor.getId())) {
- log.debug("Remove Boundary-Conflict range: {} in store {}",
- KVRangeIdUtil.toString(rangeDescriptor.getId()),
- storeDescriptor.getId());
- return quit(localStoreId, rangeDescriptor);
+ log.warn("Schedule command to remove boundary-conflict range: id={}, boundary={}",
+ KVRangeIdUtil.toString(rangeDescriptor.getId()), rangeDescriptor.getBoundary());
+ pendingQuitCommand.set(
+ new PendingQuitCommand(quit(localStoreId, rangeDescriptor), randomSuspicionTimeout()));
+ return true;
}
}
}
- return NoNeedBalance.INSTANCE;
+ return false;
}
- private Map<KVRangeId, SortedSet<LeaderRange>> findConflictingRanges(Set<KVRangeStoreDescriptor> effectiveEpoch) {
- Map<KVRangeId, SortedSet<LeaderRange>> leaderRangesByRangeId = new HashMap<>();
- Map<KVRangeId, SortedSet<LeaderRange>> conflictingRanges = new HashMap<>();
+ private Map<KVRangeId, NavigableSet<LeaderRange>> findConflictingRanges(
+ Set<KVRangeStoreDescriptor> effectiveEpoch) {
+ Map<KVRangeId, NavigableSet<LeaderRange>> leaderRangesByRangeId = new HashMap<>();
+ Map<KVRangeId, NavigableSet<LeaderRange>> conflictingRanges = new HashMap<>();
for (KVRangeStoreDescriptor storeDescriptor : effectiveEpoch) {
for (KVRangeDescriptor rangeDescriptor : storeDescriptor.getRangesList()) {
if (rangeDescriptor.getRole() != RaftNodeStatus.Leader) {
@@ -151,12 +210,42 @@ public class RedundantRangeRemovalBalancer extends StoreBalancer {
Comparator.comparing((LeaderRange lr) -> lr.ownerStoreDescriptor().getId(), String::compareTo)
.reversed()));
leaderRanges.add(new LeaderRange(rangeDescriptor, storeDescriptor));
- if (leaderRanges.size() > 1) {
- // More than one leader for the same range, add to conflicting ranges
- conflictingRanges.put(rangeId, leaderRanges);
+ }
+ }
+ for (KVRangeId rangeId : leaderRangesByRangeId.keySet()) {
+ NavigableSet<LeaderRange> leaderRanges = leaderRangesByRangeId.get(rangeId);
+ LeaderRange firstLeaderRange = leaderRanges.first();
+ ClusterConfig firstLeaderClusterConfig = firstLeaderRange.descriptor().getConfig();
+ if (leaderRanges.size() > 1) {
+ NavigableSet<LeaderRange> restLeaderRanges = leaderRanges.tailSet(firstLeaderRange, false);
+ // check if rest leader ranges are conflicting: disjoint voter set
+ for (LeaderRange restLeaderRange : restLeaderRanges) {
+ ClusterConfig restLeaderClusterConfig = restLeaderRange.descriptor().getConfig();
+ if (isDisjoint(firstLeaderClusterConfig, restLeaderClusterConfig)) {
+ // if disjoint, add to conflicting ranges
+ conflictingRanges.put(rangeId, leaderRanges);
+ }
}
}
}
return conflictingRanges;
}
+
+ private boolean isDisjoint(ClusterConfig firstConfig, ClusterConfig secondConfig) {
+ Set<String> firstVoters = Sets.newHashSet(firstConfig.getVotersList());
+ Set<String> secondVoters = Sets.newHashSet(secondConfig.getVotersList());
+ Set<String> firstNextVoters = Sets.newHashSet(firstConfig.getNextVotersList());
+ Set<String> secondNextVoters = Sets.newHashSet(secondConfig.getNextVotersList());
+ return Collections.disjoint(firstVoters, secondVoters)
+ && Collections.disjoint(firstNextVoters, secondNextVoters);
+ }
+
+ private long randomSuspicionTimeout() {
+ return millisSource.get()
+ + ThreadLocalRandom.current().nextLong(suspicionDurationMillis, suspicionDurationMillis * 2);
+ }
+
+ private record PendingQuitCommand(BalanceCommand quitCmd, long triggerTime) {
+
+ }
}
diff --git a/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/ReplicaCntBalancer.java b/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/ReplicaCntBalancer.java
index dcb47fac..7fabc17d 100644
--- a/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/ReplicaCntBalancer.java
+++ b/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/ReplicaCntBalancer.java
@@ -19,14 +19,11 @@
package org.apache.bifromq.basekv.balance.impl;
-import static com.google.common.collect.Sets.difference;
-import static com.google.common.collect.Sets.union;
-
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.google.protobuf.Struct;
import com.google.protobuf.Value;
-import java.util.Collections;
+import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
@@ -76,6 +73,17 @@ public class ReplicaCntBalancer extends RuleBasedPlacementBalancer {
Preconditions.checkArgument(validate(defaultLoadRules), "Invalid default load rules");
}
+ private ClusterConfig buildConfig(Set<String> voters, Set<String> learners) {
+ return ClusterConfig.newBuilder()
+ .addAllVoters(voters)
+ .addAllLearners(learners)
+ .build();
+ }
+
+ private void sanitize(Set<String> s, Set<String> live) {
+ s.retainAll(live);
+ }
+
@Override
public Struct initialLoadRules() {
return defaultLoadRules;
@@ -116,137 +124,190 @@ public class ReplicaCntBalancer extends RuleBasedPlacementBalancer {
Map<String, KVRangeStoreDescriptor> landscape,
EffectiveRoute effectiveRoute,
Map<Boundary, ClusterConfig> expectedRangeLayout) {
- int expectedVoters = (int) loadRules.getFieldsMap().get(LOAD_RULE_VOTERS).getNumberValue();
- int expectedLearners = (int) loadRules.getFieldsMap().get(LOAD_RULE_LEARNERS).getNumberValue();
- // meeting goal one - meet the expected number of Voter replicas and learner replicas for each Range dynamically
+ final Set<String> liveStores = landscape.keySet();
+ final int expectedVoters = (int) loadRules.getFieldsMap().get(LOAD_RULE_VOTERS).getNumberValue();
+ final int expectedLearners = (int) loadRules.getFieldsMap().get(LOAD_RULE_LEARNERS).getNumberValue();
+
+ if (liveStores.size() < expectedVoters) {
+ for (Map.Entry<Boundary, LeaderRange> e : effectiveRoute.leaderRanges().entrySet()) {
+ ClusterConfig cc = e.getValue().descriptor().getConfig();
+ for (String v : cc.getVotersList()) {
+ if (!liveStores.contains(v)) {
+ // shortcut for rolling restart
+ return true;
+ }
+ }
+ }
+ }
+
boolean meetingGoal = false;
+
for (Map.Entry<Boundary, LeaderRange> entry : effectiveRoute.leaderRanges().entrySet()) {
- Boundary boundary = entry.getKey();
LeaderRange leaderRange = entry.getValue();
KVRangeDescriptor rangeDescriptor = leaderRange.descriptor();
ClusterConfig clusterConfig = rangeDescriptor.getConfig();
- if (meetingGoal) {
- expectedRangeLayout.put(boundary, clusterConfig);
- continue;
- }
+
+ // if there is running config change process, abort generation and wait for the next round
+ // keep range config change as linear as possible
if (clusterConfig.getNextVotersCount() > 0 || clusterConfig.getNextLearnersCount() > 0) {
- // if there is running config change process, abort generation
- expectedRangeLayout.put(boundary, clusterConfig);
- meetingGoal = true;
- continue;
+ expectedRangeLayout.clear();
+ // shortcut
+ return true;
}
- // voter count not meet expectation or exceeds actual store node amount
- Set<String> voters = new HashSet<>(clusterConfig.getVotersList());
- Set<String> learners = new HashSet<>(clusterConfig.getLearnersList());
- if (clusterConfig.getVotersCount() != expectedVoters || clusterConfig.getVotersCount() > landscape.size()) {
- if (clusterConfig.getVotersCount() < expectedVoters) {
- // add some voters from the least range count store
- List<String> aliveStoresSortedByRangeCountAsc = landscape.entrySet().stream()
- .filter(e ->
- !learners.contains(e.getKey()) && !voters.contains(e.getKey()))
+
+ final Set<String> voters = new HashSet<>(clusterConfig.getVotersList());
+ final Set<String> learners = new HashSet<>(clusterConfig.getLearnersList());
+
+ // remove unreachable stores from voters and learners
+ sanitize(voters, liveStores);
+ sanitize(learners, liveStores);
+
+ Boundary boundary = entry.getKey();
+ int targetVoters = Math.min(expectedVoters, liveStores.size());
+ boolean needFix = voters.size() != targetVoters;
+ if (!meetingGoal && needFix) {
+ String leaderStore = leaderRange.ownerStoreDescriptor().getId();
+ if (voters.size() < targetVoters) {
+ if (!learners.isEmpty()) {
+ List<String> learnerCandidates = landscape.entrySet().stream()
+ .filter(e -> learners.contains(e.getKey()))
+ .sorted(Comparator.comparingInt(e -> e.getValue().getRangesCount()))
+ .map(Map.Entry::getKey)
+ .toList();
+ for (String s : learnerCandidates) {
+ learners.remove(s); // promote learner -> voter
+ voters.add(s);
+ if (voters.size() == targetVoters) {
+ break;
+ }
+ }
+ }
+
+ if (voters.size() < targetVoters) {
+ List<String> freeCandidates = landscape.entrySet().stream()
+ .filter(e -> !learners.contains(e.getKey()) && !voters.contains(e.getKey()))
+ .sorted(Comparator.comparingInt(e -> e.getValue().getRangesCount()))
+ .map(Map.Entry::getKey)
+ .toList();
+ for (String s : freeCandidates) {
+ voters.add(s);
+ if (voters.size() == targetVoters) {
+ break;
+ }
+ }
+ }
+
+ if (expectedLearners == -1) {
+ Set<String> newLearners = new HashSet<>(liveStores);
+ newLearners.removeAll(voters);
+ learners.clear();
+ learners.addAll(newLearners);
+ }
+ List<String> candidates = landscape.entrySet().stream()
+ .filter(e -> !learners.contains(e.getKey()) && !voters.contains(e.getKey()))
.sorted(Comparator.comparingInt(e -> e.getValue().getRangesCount()))
.map(Map.Entry::getKey)
.toList();
- for (String aliveStoreId : aliveStoresSortedByRangeCountAsc) {
- voters.add(aliveStoreId);
- if (voters.size() == expectedVoters) {
+ for (String s : candidates) {
+ voters.add(s);
+ if (voters.size() == targetVoters) {
break;
}
}
- } else {
- // remove some voters from the most range count store
- List<String> aliveStoresSortedByRangeCountDesc = landscape.entrySet().stream()
+ } else { // voters.size() > targetVoters
+ List<String> overloaded = landscape.entrySet().stream()
.sorted((a, b) -> b.getValue().getRangesCount() - a.getValue().getRangesCount())
.map(Map.Entry::getKey)
.toList();
- for (String aliveStoreId : aliveStoresSortedByRangeCountDesc) {
- if (!aliveStoreId.equals(leaderRange.ownerStoreDescriptor().getId())) {
- voters.remove(aliveStoreId);
+ for (String s : overloaded) {
+ if (!s.equals(leaderStore) && voters.contains(s)) {
+ voters.remove(s);
+ if (voters.size() == targetVoters) {
+ break;
+ }
}
- if (voters.size() == expectedVoters) {
- break;
+ }
+ if (voters.size() > targetVoters) {
+ for (String s : new ArrayList<>(voters)) {
+ if (!s.equals(leaderStore)) {
+ voters.remove(s);
+ if (voters.size() == targetVoters) {
+ break;
+ }
+ }
}
}
}
- // remove unreachable voters
- voters.removeIf(voter -> !landscape.containsKey(voter));
- ClusterConfig newConfig = ClusterConfig.newBuilder()
- .mergeFrom(clusterConfig)
- .clearVoters()
- .addAllVoters(voters)
- .build();
- if (!newConfig.equals(clusterConfig)) {
- meetingGoal = true;
- }
- expectedRangeLayout.put(boundary, newConfig);
+ expectedRangeLayout.put(boundary, buildConfig(voters, learners));
+ meetingGoal = true;
} else {
- expectedRangeLayout.put(boundary, clusterConfig);
+ expectedRangeLayout.put(boundary, buildConfig(voters, learners));
}
}
+
if (meetingGoal) {
return true;
}
- // voter count met the expectation, check learner count
+
for (Map.Entry<Boundary, LeaderRange> entry : effectiveRoute.leaderRanges().entrySet()) {
- Boundary boundary = entry.getKey();
LeaderRange leaderRange = entry.getValue();
KVRangeDescriptor rangeDescriptor = leaderRange.descriptor();
ClusterConfig clusterConfig = rangeDescriptor.getConfig();
- if (meetingGoal) {
- expectedRangeLayout.put(boundary, clusterConfig);
- continue;
- }
+
Set<String> voters = new HashSet<>(clusterConfig.getVotersList());
Set<String> learners = new HashSet<>(clusterConfig.getLearnersList());
- if (expectedLearners == -1
- || clusterConfig.getLearnersCount() != expectedLearners
- || clusterConfig.getLearnersCount() > landscape.size()) {
- if (expectedLearners == -1) {
- Set<String> newLearners = new HashSet<>(landscape.keySet());
- newLearners.removeAll(voters);
- learners.addAll(newLearners);
- } else {
- if (clusterConfig.getLearnersCount() < expectedLearners) {
- // add some learners from the least range count store
- List<String> aliveStoresSortedByRangeCountAsc = landscape.entrySet().stream()
- .sorted(Comparator.comparingInt(e -> e.getValue().getRangesCount()))
- .map(Map.Entry::getKey)
- .toList();
- for (String aliveStoreId : aliveStoresSortedByRangeCountAsc) {
- if (!voters.contains(aliveStoreId)) {
- learners.add(aliveStoreId);
- }
- if (learners.size() == expectedVoters) {
+ sanitize(voters, liveStores);
+ sanitize(learners, liveStores);
+
+ boolean changed = false;
+
+ if (expectedLearners == -1) {
+ // learners = live - voters
+ Set<String> newLearners = new HashSet<>(liveStores);
+ newLearners.removeAll(voters);
+ if (!newLearners.equals(learners)) {
+ learners = newLearners;
+ changed = true;
+ }
+ } else {
+ int maxPossible = Math.max(0, liveStores.size() - voters.size());
+ int targetLearners = Math.min(expectedLearners, maxPossible);
+
+ if (learners.size() < targetLearners) {
+ List<String> candidates = landscape.entrySet().stream()
+ .sorted(Comparator.comparingInt(e -> e.getValue().getRangesCount()))
+ .map(Map.Entry::getKey)
+ .toList();
+ for (String s : candidates) {
+ if (!voters.contains(s) && !learners.contains(s)) {
+ learners.add(s);
+ if (learners.size() == targetLearners) {
break;
}
}
- } else {
- // remove some learners from the most range count store
- List<String> aliveStoresSortedByRangeCountDesc = landscape.entrySet().stream()
- .sorted((a, b) -> b.getValue().getRangesCount() - a.getValue().getRangesCount())
- .map(Map.Entry::getKey)
- .toList();
- for (String aliveStoreId : aliveStoresSortedByRangeCountDesc) {
- learners.remove(aliveStoreId);
- if (learners.size() == expectedLearners) {
+ }
+ changed = true;
+ } else if (learners.size() > targetLearners) {
+ List<String> overloaded = landscape.entrySet().stream()
+ .sorted((a, b) -> b.getValue().getRangesCount() - a.getValue().getRangesCount())
+ .map(Map.Entry::getKey)
+ .toList();
+ for (String s : overloaded) {
+ if (learners.contains(s)) {
+ learners.remove(s);
+ if (learners.size() == targetLearners) {
break;
}
}
}
+ changed = true;
}
- // remove unreachable learners
- learners.removeIf(learner -> !landscape.containsKey(learner));
- ClusterConfig newConfig = ClusterConfig.newBuilder()
- .mergeFrom(clusterConfig)
- .clearLearners()
- .addAllLearners(learners)
- .build();
- if (!newConfig.equals(clusterConfig)) {
- meetingGoal = true;
- }
- expectedRangeLayout.put(boundary, newConfig);
- } else {
- expectedRangeLayout.put(boundary, clusterConfig);
+ }
+
+ Boundary boundary = entry.getKey();
+ expectedRangeLayout.put(boundary, buildConfig(voters, learners));
+ if (!meetingGoal && changed) {
+ meetingGoal = true;
}
}
return meetingGoal;
@@ -255,133 +316,148 @@ public class ReplicaCntBalancer extends RuleBasedPlacementBalancer {
private boolean balanceVoterCount(Map<String, KVRangeStoreDescriptor> landscape,
EffectiveRoute effectiveRoute,
Map<Boundary, ClusterConfig> expectedRangeLayout) {
- // goal one has met, meeting goal two - evenly distributed voter replicas across all stores
- boolean meetingGoal = false;
+ final Set<String> liveStores = landscape.keySet();
Map<String, Integer> storeVoterCount = new HashMap<>();
for (Map.Entry<Boundary, LeaderRange> entry : effectiveRoute.leaderRanges().entrySet()) {
ClusterConfig config = entry.getValue().descriptor().getConfig();
- config.getVotersList()
+ config.getVotersList().stream()
+ .filter(liveStores::contains)
.forEach(storeId -> storeVoterCount.put(storeId, storeVoterCount.getOrDefault(storeId, 0) + 1));
}
- landscape.keySet().forEach(storeId -> {
- if (!storeVoterCount.containsKey(storeId)) {
- storeVoterCount.put(storeId, 0);
- }
- });
- record StoreVoterCount(String storeId, int voterCount) {
- }
+ liveStores.forEach(s -> storeVoterCount.putIfAbsent(s, 0));
+
+ record StoreVoterCount(String storeId, int voterCount) {}
- SortedSet<StoreVoterCount> storeVoterCountSorted = new TreeSet<>(Comparator
- .comparingInt(StoreVoterCount::voterCount).thenComparing(StoreVoterCount::storeId));
+ SortedSet<StoreVoterCount> storeVoterCountSorted = new TreeSet<>(
+ Comparator.comparingInt(StoreVoterCount::voterCount).thenComparing(StoreVoterCount::storeId));
storeVoterCount.forEach(
(storeId, voterCount) -> storeVoterCountSorted.add(new StoreVoterCount(storeId, voterCount)));
+
double totalVoters = storeVoterCount.values().stream().mapToInt(Integer::intValue).sum();
- double targetVotersPerStore = totalVoters / landscape.size();
- int maxVotersPerStore = (int) Math.ceil(targetVotersPerStore);
+ double targetVotersPerStore = liveStores.isEmpty() ? 0 : totalVoters / liveStores.size();
int minVotersPerStore = (int) Math.floor(targetVotersPerStore);
- int globalMax = Collections.max(storeVoterCount.values());
- int globalMin = Collections.min(storeVoterCount.values());
+ int globalMax = storeVoterCount.values().stream().mapToInt(Integer::intValue).max().orElse(0);
+ int globalMin = storeVoterCount.values().stream().mapToInt(Integer::intValue).min().orElse(0);
if (globalMax - globalMin <= 1) {
return false;
}
+ boolean meetingGoal = false;
for (Map.Entry<Boundary, LeaderRange> entry : effectiveRoute.leaderRanges().entrySet()) {
Boundary boundary = entry.getKey();
- LeaderRange leaderRange = entry.getValue();
- KVRangeDescriptor rangeDescriptor = leaderRange.descriptor();
- ClusterConfig clusterConfig = rangeDescriptor.getConfig();
- if (meetingGoal) {
- expectedRangeLayout.put(boundary, clusterConfig);
- continue;
- }
- // examine in sorted order to ensure the result is deterministic
- Set<String> learners = Sets.newHashSet(clusterConfig.getLearnersList());
- SortedSet<String> voterSorted = Sets.newTreeSet(clusterConfig.getVotersList());
- for (String voter : voterSorted) {
- if (storeVoterCount.get(voter) >= maxVotersPerStore) {
- // voter store has overloaded voters
- for (StoreVoterCount underloadedStore : storeVoterCountSorted) {
- // move to one underloaded store which is current not in the voter list
- if (storeVoterCount.get(underloadedStore.storeId) <= minVotersPerStore
- && !voterSorted.contains(underloadedStore.storeId)
- && !learners.contains(underloadedStore.storeId)) {
- meetingGoal = true;
- ClusterConfig newConfig = ClusterConfig.newBuilder()
- .addAllVoters(
- difference(union(voterSorted, Set.of(underloadedStore.storeId)), Set.of(voter)))
- .addAllLearners(learners)
- .build();
- expectedRangeLayout.put(boundary, newConfig);
- break;
+ LeaderRange lr = entry.getValue();
+ ClusterConfig cc = lr.descriptor().getConfig();
+
+ Set<String> learners = Sets.newHashSet(cc.getLearnersList());
+ SortedSet<String> voterSorted = Sets.newTreeSet(cc.getVotersList());
+ sanitize(learners, liveStores);
+ voterSorted.retainAll(liveStores);
+
+ if (!meetingGoal) {
+ meet:
+ for (String voter : new ArrayList<>(voterSorted)) {
+ int voters = storeVoterCount.getOrDefault(voter, 0);
+ if (voters == globalMax) {
+ for (StoreVoterCount under : storeVoterCountSorted) {
+ if (storeVoterCount.getOrDefault(under.storeId, 0) <= minVotersPerStore
+ && !voterSorted.contains(under.storeId)
+ && !learners.contains(under.storeId)) {
+ // move voter -> underloaded
+ Set<String> newVoters = new HashSet<>(voterSorted);
+ newVoters.remove(voter);
+ newVoters.add(under.storeId);
+
+ expectedRangeLayout.put(boundary, buildConfig(newVoters, learners));
+ meetingGoal = true;
+ break meet;
+ }
}
}
}
+ if (!meetingGoal) {
+ expectedRangeLayout.put(boundary, buildConfig(voterSorted, learners));
+ }
+ } else {
+ expectedRangeLayout.put(boundary, buildConfig(voterSorted, learners));
}
}
+ if (!meetingGoal) {
+ expectedRangeLayout.clear();
+ }
return meetingGoal;
}
- private boolean balanceLearnerCount(Map<String, KVRangeStoreDescriptor> landscape,
- EffectiveRoute effectiveRoute,
- Map<Boundary, ClusterConfig> expectedRangeLayout) {
- boolean meetingGoal = false;
+ private void balanceLearnerCount(Map<String, KVRangeStoreDescriptor> landscape,
+ EffectiveRoute effectiveRoute,
+ Map<Boundary, ClusterConfig> expectedRangeLayout) {
+ final Set<String> liveStores = landscape.keySet();
+
Map<String, Integer> storeLearnerCount = new HashMap<>();
for (Map.Entry<Boundary, LeaderRange> entry : effectiveRoute.leaderRanges().entrySet()) {
ClusterConfig config = entry.getValue().descriptor().getConfig();
- config.getLearnersList()
+ config.getLearnersList().stream()
+ .filter(liveStores::contains)
.forEach(storeId -> storeLearnerCount.put(storeId, storeLearnerCount.getOrDefault(storeId, 0) + 1));
}
- landscape.keySet().forEach(storeId -> {
- if (!storeLearnerCount.containsKey(storeId)) {
- storeLearnerCount.put(storeId, 0);
- }
- });
- record StoreLearnerCount(String storeId, int voterCount) {
- }
+ liveStores.forEach(s -> storeLearnerCount.putIfAbsent(s, 0));
+
+ record StoreLearnerCount(String storeId, int learnerCount) {}
- SortedSet<StoreLearnerCount> storeVoterCountSorted = new TreeSet<>(Comparator
- .comparingInt(StoreLearnerCount::voterCount).thenComparing(StoreLearnerCount::storeId));
- storeLearnerCount.forEach(
- (storeId, voterCount) -> storeVoterCountSorted.add(new StoreLearnerCount(storeId, voterCount)));
+ SortedSet<StoreLearnerCount> storeLearnerCountSorted = new TreeSet<>(
+ Comparator.comparingInt(StoreLearnerCount::learnerCount).thenComparing(StoreLearnerCount::storeId));
+ storeLearnerCount.forEach((id, c) -> storeLearnerCountSorted.add(new StoreLearnerCount(id, c)));
double totalLearners = storeLearnerCount.values().stream().mapToInt(Integer::intValue).sum();
- double targetLearnersPerStore = totalLearners / landscape.size();
- int maxLearnersPerStore = (int) Math.ceil(targetLearnersPerStore);
+ double targetLearnersPerStore = liveStores.isEmpty() ? 0 : totalLearners / liveStores.size();
+ int minLearnersPerStore = (int) Math.floor(targetLearnersPerStore);
+
+ int globalMax = storeLearnerCount.values().stream().mapToInt(Integer::intValue).max().orElse(0);
+ int globalMin = storeLearnerCount.values().stream().mapToInt(Integer::intValue).min().orElse(0);
+ if (globalMax - globalMin <= 1) {
+ return;
+ }
+ boolean meetingGoal = false;
for (Map.Entry<Boundary, LeaderRange> entry : effectiveRoute.leaderRanges().entrySet()) {
Boundary boundary = entry.getKey();
- LeaderRange leaderRange = entry.getValue();
- KVRangeDescriptor rangeDescriptor = leaderRange.descriptor();
- ClusterConfig clusterConfig = rangeDescriptor.getConfig();
- if (meetingGoal) {
- expectedRangeLayout.put(boundary, clusterConfig);
- continue;
- }
- // examine in sorted order to ensure the result is deterministic
- Set<String> voters = Sets.newHashSet(clusterConfig.getVotersList());
- SortedSet<String> learnerSorted = Sets.newTreeSet(clusterConfig.getLearnersList());
- for (String learner : learnerSorted) {
- if (storeLearnerCount.get(learner) > maxLearnersPerStore) {
- // learner store has overloaded learners
- for (StoreLearnerCount underloadedStore : storeVoterCountSorted) {
- // move to one underloaded store which is current not in the voter or learner list
- if (storeLearnerCount.get(underloadedStore.storeId) < maxLearnersPerStore
- && !voters.contains(underloadedStore.storeId)
- && !learnerSorted.contains(underloadedStore.storeId)) {
- meetingGoal = true;
- ClusterConfig newConfig = ClusterConfig.newBuilder()
- .addAllVoters(voters)
- .addAllLearners(difference(
- union(learnerSorted, Set.of(underloadedStore.storeId)), Set.of(learner)))
- .build();
- expectedRangeLayout.put(boundary, newConfig);
- break;
+ LeaderRange lr = entry.getValue();
+ ClusterConfig cc = lr.descriptor().getConfig();
+
+ Set<String> voters = Sets.newHashSet(cc.getVotersList());
+ SortedSet<String> learnerSorted = Sets.newTreeSet(cc.getLearnersList());
+ sanitize(voters, liveStores);
+ learnerSorted.retainAll(liveStores);
+
+ if (!meetingGoal) {
+ meet:
+ for (String learner : new ArrayList<>(learnerSorted)) {
+ int learners = storeLearnerCount.getOrDefault(learner, 0);
+ if (learners == globalMax) {
+ for (StoreLearnerCount under : storeLearnerCountSorted) {
+ if (storeLearnerCount.getOrDefault(under.storeId, 0) < minLearnersPerStore
+ && !voters.contains(under.storeId)
+ && !learnerSorted.contains(under.storeId)) {
+ Set<String> newLearners = new HashSet<>(learnerSorted);
+ newLearners.remove(learner);
+ newLearners.add(under.storeId);
+
+ expectedRangeLayout.put(boundary, buildConfig(voters, newLearners));
+ meetingGoal = true;
+ break meet;
+ }
}
}
}
+ if (!meetingGoal) {
+ expectedRangeLayout.put(boundary, buildConfig(voters, learnerSorted));
+ }
+ } else {
+ expectedRangeLayout.put(boundary, buildConfig(voters, learnerSorted));
}
}
- return meetingGoal;
+ if (!meetingGoal) {
+ expectedRangeLayout.clear();
+ }
}
}
diff --git a/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/UnreachableReplicaRemovalBalancer.java b/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/UnreachableReplicaRemovalBalancer.java
index b3ea01c5..abf7b204 100644
--- a/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/UnreachableReplicaRemovalBalancer.java
+++ b/base-kv/base-kv-store-balance-controller/src/main/java/org/apache/bifromq/basekv/balance/impl/UnreachableReplicaRemovalBalancer.java
@@ -14,13 +14,21 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basekv.balance.impl;
import static org.apache.bifromq.basekv.proto.State.StateType.Normal;
+import com.google.common.collect.Sets;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
import org.apache.bifromq.basehlc.HLC;
import org.apache.bifromq.basekv.balance.BalanceNow;
import org.apache.bifromq.basekv.balance.BalanceResult;
@@ -34,14 +42,6 @@ import org.apache.bifromq.basekv.raft.proto.ClusterConfig;
import org.apache.bifromq.basekv.raft.proto.RaftNodeStatus;
import org.apache.bifromq.basekv.raft.proto.RaftNodeSyncState;
import org.apache.bifromq.basekv.utils.KVRangeIdUtil;
-import com.google.common.collect.Sets;
-import java.time.Duration;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Supplier;
/**
* The UnreachableReplicaRemovalBalancer is a specialized balancer responsible for managing and removing unreachable
@@ -99,7 +99,10 @@ public final class UnreachableReplicaRemovalBalancer extends StoreBalancer {
public void update(Set<KVRangeStoreDescriptor> landscape) {
Map<String, Map<KVRangeId, KVRangeDescriptor>> descriptorMap = build(landscape);
latestDescriptorMap = descriptorMap;
-
+ if (!descriptorMap.containsKey(localStoreId)) {
+ replicaSuspicionTimeMap.clear();
+ return; // No need to process if local store is not present in the landscape
+ }
// Track the current leaders
Set<KVRangeId> currentLeaders = new HashSet<>();
diff --git a/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/KVStoreBalanceControllerTest.java b/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/KVStoreBalanceControllerTest.java
index e3f53796..dc44b4be 100644
--- a/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/KVStoreBalanceControllerTest.java
+++ b/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/KVStoreBalanceControllerTest.java
@@ -79,6 +79,7 @@ public class KVStoreBalanceControllerTest {
private static final String LOCAL_STORE_ID = "localStoreId";
private final PublishSubject<Map<String, BalancerStateSnapshot>> proposalSubject = PublishSubject.create();
private final PublishSubject<Set<KVRangeStoreDescriptor>> storeDescSubject = PublishSubject.create();
+ private final PublishSubject<Long> refreshSignal = PublishSubject.create();
@Mock
private IBaseKVMetaService metaService;
@Mock
@@ -103,6 +104,7 @@ public class KVStoreBalanceControllerTest {
when(balancerFactory.newBalancer(eq(CLUSTER_ID), eq(LOCAL_STORE_ID))).thenReturn(storeBalancer);
when(metaService.balancerStatesProposal(eq(CLUSTER_ID))).thenReturn(statesProposal);
when(metaService.balancerStatesReporter(eq(CLUSTER_ID), eq(LOCAL_STORE_ID))).thenReturn(statesReporter);
+ when(statesReporter.refreshSignal()).thenReturn(refreshSignal);
when(statesProposal.expectedBalancerStates()).thenReturn(proposalSubject);
when(storeClient.describe()).thenReturn(storeDescSubject);
executor = Executors.newScheduledThreadPool(1);
@@ -400,6 +402,18 @@ public class KVStoreBalanceControllerTest {
verify(statesReporter, never()).reportBalancerState(anyString(), anyBoolean(), any(Struct.class));
}
+ @Test
+ public void testRefreshSignal() {
+ reset(statesReporter);
+ refreshSignal.onNext(System.currentTimeMillis());
+ verify(statesReporter, times(1))
+ .reportBalancerState(anyString(), anyBoolean(), any(Struct.class));
+ verify(statesReporter, times(1))
+ .reportBalancerState(eq(balancerFactory.getClass().getName()),
+ eq(false),
+ eq(Struct.getDefaultInstance()));
+ }
+
private Set<KVRangeStoreDescriptor> generateDescriptor(KVRangeId id, long ver) {
List<String> voters = Lists.newArrayList(LOCAL_STORE_ID, "store1");
List<String> learners = Lists.newArrayList();
diff --git a/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RangeBootstrapBalancerTest.java b/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RangeBootstrapBalancerTest.java
index 6c9aec88..7e167a8f 100644
--- a/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RangeBootstrapBalancerTest.java
+++ b/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RangeBootstrapBalancerTest.java
@@ -21,18 +21,20 @@ package org.apache.bifromq.basekv.balance.impl;
import static org.apache.bifromq.basekv.utils.BoundaryUtil.FULL_BOUNDARY;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
-import org.apache.bifromq.basekv.balance.BalanceNow;
-import org.apache.bifromq.basekv.balance.BalanceResult;
-import org.apache.bifromq.basekv.balance.BalanceResultType;
-import org.apache.bifromq.basekv.balance.command.BootstrapCommand;
-import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
import java.time.Duration;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
+import org.apache.bifromq.basekv.balance.BalanceNow;
+import org.apache.bifromq.basekv.balance.BalanceResult;
+import org.apache.bifromq.basekv.balance.BalanceResultType;
+import org.apache.bifromq.basekv.balance.command.BootstrapCommand;
+import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -58,7 +60,7 @@ public class RangeBootstrapBalancerTest {
BalanceResult result = balancer.balance();
assertSame(result.type(), BalanceResultType.BalanceNow);
- assertEquals(FULL_BOUNDARY, ((BootstrapCommand) ((BalanceNow<?>) result).command).getBoundary());
+ assertEquals(((BootstrapCommand) ((BalanceNow<?>) result).command).getBoundary(), FULL_BOUNDARY);
}
@@ -74,6 +76,51 @@ public class RangeBootstrapBalancerTest {
BalanceResult result = balancer.balance();
assertSame(result.type(), BalanceResultType.BalanceNow);
- assertEquals(FULL_BOUNDARY, ((BootstrapCommand) ((BalanceNow<?>) result).command).getBoundary());
+ assertEquals(((BootstrapCommand) ((BalanceNow<?>) result).command).getBoundary(), FULL_BOUNDARY);
+ }
+
+ @Test
+ public void returnsAwaitImmediatelyBeforeDeadline() {
+ balancer.update(Collections.emptySet());
+
+ BalanceResult result = balancer.balance();
+ assertSame(result.type(), BalanceResultType.AwaitBalance);
+
+ Duration remaining = ((org.apache.bifromq.basekv.balance.AwaitBalance) result).await;
+ assertFalse(remaining.isZero());
+ assertTrue(remaining.toMillis() <= 2000L);
+ }
+
+ @Test
+ public void awaitThenBalanceNowAfterDeadline() {
+ balancer.update(Collections.emptySet());
+
+ BalanceResult r1 = balancer.balance();
+ assertSame(r1.type(), BalanceResultType.AwaitBalance);
+ long r1ms = ((org.apache.bifromq.basekv.balance.AwaitBalance) r1).await.toMillis();
+ assertTrue(r1ms > 0);
+
+ long half = Math.max(1, r1ms / 2);
+ mockTime.addAndGet(half);
+ BalanceResult r2 = balancer.balance();
+ assertSame(r2.type(), BalanceResultType.AwaitBalance);
+ long r2ms = ((org.apache.bifromq.basekv.balance.AwaitBalance) r2).await.toMillis();
+ assertTrue(r2ms >= 0 && r2ms < r1ms);
+
+ mockTime.addAndGet(r2ms + 1);
+ BalanceResult r3 = balancer.balance();
+ assertSame(r3.type(), BalanceResultType.BalanceNow);
+ assertEquals(((BootstrapCommand) ((BalanceNow<?>) r3).command).getBoundary(), FULL_BOUNDARY);
+ }
+
+ @Test
+ public void noSecondTriggerAfterBootstrapFires() {
+ balancer.update(Collections.emptySet());
+ mockTime.addAndGet(2000L);
+ BalanceResult fired = balancer.balance();
+ assertSame(fired.type(), BalanceResultType.BalanceNow);
+
+ BalanceResult next = balancer.balance();
+ assertSame(next.type(), BalanceResultType.NoNeedBalance);
}
}
\ No newline at end of file
diff --git a/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RangeLeaderBalancerTest.java b/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RangeLeaderBalancerTest.java
index 1b1aebcc..ab1348c7 100644
--- a/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RangeLeaderBalancerTest.java
+++ b/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RangeLeaderBalancerTest.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basekv.balance.impl;
@@ -25,6 +25,8 @@ import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;
+import com.google.protobuf.ByteString;
+import java.util.Set;
import org.apache.bifromq.basekv.balance.BalanceNow;
import org.apache.bifromq.basekv.balance.BalanceResult;
import org.apache.bifromq.basekv.balance.BalanceResultType;
@@ -34,10 +36,9 @@ import org.apache.bifromq.basekv.proto.Boundary;
import org.apache.bifromq.basekv.proto.KVRangeDescriptor;
import org.apache.bifromq.basekv.proto.KVRangeId;
import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
+import org.apache.bifromq.basekv.proto.State;
import org.apache.bifromq.basekv.raft.proto.ClusterConfig;
import org.apache.bifromq.basekv.raft.proto.RaftNodeStatus;
-import com.google.protobuf.ByteString;
-import java.util.Set;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -58,6 +59,7 @@ public class RangeLeaderBalancerTest {
KVRangeDescriptor kvRangeDescriptor = KVRangeDescriptor.newBuilder()
.setId(kvRangeId)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setBoundary(
Boundary.newBuilder().setStartKey(ByteString.copyFromUtf8("a")).setEndKey(ByteString.copyFromUtf8("z"))
.build())
@@ -139,6 +141,7 @@ public class RangeLeaderBalancerTest {
KVRangeDescriptor kvRangeDescriptor1 = KVRangeDescriptor.newBuilder()
.setId(kvRangeId1)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setBoundary(toBoundary(null, ByteString.copyFromUtf8("z")))
.setConfig(ClusterConfig.newBuilder().addVoters(localStoreId).addLearners("otherStore").build())
.build();
@@ -148,6 +151,7 @@ public class RangeLeaderBalancerTest {
.setId(kvRangeId2)
.setVer(1)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setBoundary(toBoundary(ByteString.copyFromUtf8("z"), null))
.setConfig(ClusterConfig.newBuilder().addVoters(localStoreId).addVoters("otherStore").build())
.build();
@@ -161,6 +165,7 @@ public class RangeLeaderBalancerTest {
KVRangeDescriptor kvRangeDescriptor3 = KVRangeDescriptor.newBuilder()
.setId(kvRangeId3)
.setRole(RaftNodeStatus.Follower)
+ .setState(State.StateType.Normal)
.setBoundary(toBoundary(ByteString.copyFromUtf8("z"), null))
.setConfig(ClusterConfig.newBuilder().addVoters("otherStore").build())
.build();
@@ -185,6 +190,7 @@ public class RangeLeaderBalancerTest {
KVRangeDescriptor kvRangeDescriptor1 = KVRangeDescriptor.newBuilder()
.setId(kvRangeId1)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setBoundary(Boundary.newBuilder().setEndKey(ByteString.copyFromUtf8("z")).build())
.setConfig(ClusterConfig.newBuilder().addVoters(localStoreId).addLearners("otherStore").build())
.build();
@@ -193,6 +199,7 @@ public class RangeLeaderBalancerTest {
.setId(kvRangeId2)
.setVer(1)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setBoundary(Boundary.newBuilder().setStartKey(ByteString.copyFromUtf8("z")).build())
.setConfig(ClusterConfig.newBuilder().addVoters(localStoreId).addVoters("otherStore").build())
.build();
@@ -205,6 +212,7 @@ public class RangeLeaderBalancerTest {
KVRangeDescriptor kvRangeDescriptor3 = KVRangeDescriptor.newBuilder()
.setId(kvRangeId3)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setBoundary(Boundary.newBuilder().setStartKey(ByteString.copyFromUtf8("z")).build())
.setConfig(ClusterConfig.newBuilder().addVoters("otherStore").build())
.build();
diff --git a/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RangeSplitBalancerTest.java b/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RangeSplitBalancerTest.java
index a43f8fd4..0c0d6b62 100644
--- a/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RangeSplitBalancerTest.java
+++ b/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RangeSplitBalancerTest.java
@@ -163,4 +163,176 @@ public class RangeSplitBalancerTest {
.build()));
assertEquals(balancer.balance().type(), BalanceResultType.NoNeedBalance);
}
+
+ @Test
+ public void skipWhenConfigHasDeadVoter() {
+ RangeSplitBalancer balancer = new RangeSplitBalancer(clusterId, "store1", HintType, 30, 0.8, 30, 30_000);
+
+ KVRangeDescriptor rd = rangeDescriptorBuilder
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters("store1")
+ .addVoters("deadStore")
+ .build())
+ .addHints(SplitHint.newBuilder()
+ .setType(HintType)
+ .putLoad("ioDensity", 40)
+ .putLoad("ioLatencyNanos", 100)
+ .setSplitKey(ByteString.copyFromUtf8("a"))
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor sd = storeDescriptorBuilder
+ .clearRanges()
+ .addRanges(rd)
+ .putStatistics("cpu.usage", 0.7)
+ .build();
+
+ balancer.update(Set.of(sd));
+
+ assertEquals(balancer.balance().type(), BalanceResultType.NoNeedBalance);
+ }
+
+ @Test
+ public void skipWhenConfigHasDeadLearner() {
+ RangeSplitBalancer balancer = new RangeSplitBalancer(clusterId, "store1", HintType, 30, 0.8, 30, 30_000);
+
+ KVRangeDescriptor rd = rangeDescriptorBuilder
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters("store1")
+ .addLearners("ghost")
+ .build())
+ .addHints(SplitHint.newBuilder()
+ .setType(HintType)
+ .putLoad("ioDensity", 40)
+ .putLoad("ioLatencyNanos", 100)
+ .setSplitKey(ByteString.copyFromUtf8("a"))
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor sd = storeDescriptorBuilder
+ .clearRanges()
+ .addRanges(rd)
+ .putStatistics("cpu.usage", 0.7)
+ .build();
+
+ balancer.update(Set.of(sd));
+ assertEquals(balancer.balance().type(), BalanceResultType.NoNeedBalance);
+ }
+
+ @Test
+ public void skipWhenConfigHasDeadNextMembers() {
+ RangeSplitBalancer balancer = new RangeSplitBalancer(clusterId, "store1", HintType, 30, 0.8, 30, 30_000);
+
+ KVRangeDescriptor rd = rangeDescriptorBuilder
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters("store1")
+ .addNextVoters("deadV")
+ .addNextLearners("deadL")
+ .build())
+ .addHints(SplitHint.newBuilder()
+ .setType(HintType)
+ .putLoad("ioDensity", 40)
+ .putLoad("ioLatencyNanos", 100)
+ .setSplitKey(ByteString.copyFromUtf8("a"))
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor sd = storeDescriptorBuilder
+ .clearRanges()
+ .addRanges(rd)
+ .putStatistics("cpu.usage", 0.7)
+ .build();
+
+ balancer.update(Set.of(sd));
+ assertEquals(balancer.balance().type(), BalanceResultType.NoNeedBalance);
+ }
+
+ @Test
+ public void skipWhenOngoingConfigChange() {
+ RangeSplitBalancer balancer = new RangeSplitBalancer(clusterId, "store1", HintType, 30, 0.8, 30, 30_000);
+
+ KVRangeDescriptor rd = rangeDescriptorBuilder
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters("store1")
+ .addNextVoters("store1")
+ .build())
+ .addHints(SplitHint.newBuilder()
+ .setType(HintType)
+ .putLoad("ioDensity", 40)
+ .putLoad("ioLatencyNanos", 100)
+ .setSplitKey(ByteString.copyFromUtf8("a"))
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor sd = storeDescriptorBuilder
+ .clearRanges()
+ .addRanges(rd)
+ .putStatistics("cpu.usage", 0.7)
+ .build();
+
+ balancer.update(Set.of(sd));
+ assertEquals(balancer.balance().type(), BalanceResultType.NoNeedBalance);
+ }
+
+ @Test
+ public void skipWhenSplitKeyEqualsStartOrOutOfRange() {
+ RangeSplitBalancer balancer = new RangeSplitBalancer(clusterId, "store1", HintType, 30, 0.8, 30, 30_000);
+
+ KVRangeDescriptor rd = rangeDescriptorBuilder
+ .setBoundary(org.apache.bifromq.basekv.proto.Boundary.newBuilder()
+ .setStartKey(ByteString.copyFromUtf8("a"))
+ .setEndKey(ByteString.copyFromUtf8("z"))
+ .build())
+ .setConfig(ClusterConfig.newBuilder().addVoters("store1").build())
+ .addHints(SplitHint.newBuilder()
+ .setType(HintType)
+ .putLoad("ioDensity", 40)
+ .putLoad("ioLatencyNanos", 100)
+ .setSplitKey(ByteString.copyFromUtf8("a"))
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor sd = storeDescriptorBuilder
+ .clearRanges()
+ .addRanges(rd)
+ .putStatistics("cpu.usage", 0.7)
+ .build();
+
+ balancer.update(Set.of(sd));
+ assertEquals(balancer.balance().type(), BalanceResultType.NoNeedBalance);
+
+ KVRangeDescriptor rd2 = rd.toBuilder().clearHints()
+ .addHints(SplitHint.newBuilder()
+ .setType(HintType)
+ .putLoad("ioDensity", 40)
+ .putLoad("ioLatencyNanos", 100)
+ .setSplitKey(ByteString.copyFromUtf8("z"))
+ .build())
+ .build();
+ KVRangeStoreDescriptor sd2 = sd.toBuilder().clearRanges().addRanges(rd2).build();
+ balancer.update(Set.of(sd2));
+ assertEquals(balancer.balance().type(), BalanceResultType.NoNeedBalance);
+ }
+
+ @Test
+ public void stopSplitWhenSplitKeyNotProvided() {
+ RangeSplitBalancer balancer = new RangeSplitBalancer(clusterId, "store1", HintType, 30, 0.8, 30, 30_000);
+
+ KVRangeDescriptor rd = rangeDescriptorBuilder
+ .addHints(SplitHint.newBuilder()
+ .setType(HintType)
+ .putLoad("ioDensity", 40)
+ .putLoad("ioLatencyNanos", 100)
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor sd = storeDescriptorBuilder
+ .clearRanges()
+ .addRanges(rd)
+ .putStatistics("cpu.usage", 0.7)
+ .build();
+
+ balancer.update(Set.of(sd));
+ assertEquals(balancer.balance().type(), BalanceResultType.NoNeedBalance);
+ }
}
diff --git a/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RedundantRangeRemovalBalancerTest.java b/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RedundantRangeRemovalBalancerTest.java
index ecfeca3b..00871e26 100644
--- a/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RedundantRangeRemovalBalancerTest.java
+++ b/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/RedundantRangeRemovalBalancerTest.java
@@ -23,9 +23,11 @@ import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertSame;
import com.google.protobuf.ByteString;
+import java.time.Duration;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
import org.apache.bifromq.basekv.balance.BalanceNow;
import org.apache.bifromq.basekv.balance.BalanceResult;
import org.apache.bifromq.basekv.balance.BalanceResultType;
@@ -34,20 +36,22 @@ import org.apache.bifromq.basekv.proto.Boundary;
import org.apache.bifromq.basekv.proto.KVRangeDescriptor;
import org.apache.bifromq.basekv.proto.KVRangeId;
import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
+import org.apache.bifromq.basekv.proto.State;
import org.apache.bifromq.basekv.raft.proto.ClusterConfig;
import org.apache.bifromq.basekv.raft.proto.RaftNodeStatus;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class RedundantRangeRemovalBalancerTest {
-
private final String clusterId = "testCluster";
private final String localStoreId = "localStore";
private RedundantRangeRemovalBalancer balancer;
+ private AtomicLong mockTime;
@BeforeMethod
public void setUp() {
- balancer = new RedundantRangeRemovalBalancer(clusterId, localStoreId);
+ mockTime = new AtomicLong(0L); // Start time at 0
+ balancer = new RedundantRangeRemovalBalancer(clusterId, localStoreId, Duration.ofSeconds(1), mockTime::get);
}
@Test
@@ -56,6 +60,7 @@ public class RedundantRangeRemovalBalancerTest {
KVRangeDescriptor kvRangeDescriptor = KVRangeDescriptor.newBuilder()
.setId(kvRangeId)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setVer(1)
.setBoundary(Boundary.newBuilder()
.setStartKey(ByteString.copyFromUtf8("a"))
@@ -85,6 +90,7 @@ public class RedundantRangeRemovalBalancerTest {
KVRangeDescriptor kvRangeDescriptor1 = KVRangeDescriptor.newBuilder()
.setId(kvRangeId1)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setVer(1)
.setBoundary(Boundary.newBuilder()
.setStartKey(ByteString.copyFromUtf8("a"))
@@ -99,6 +105,7 @@ public class RedundantRangeRemovalBalancerTest {
KVRangeDescriptor kvRangeDescriptor2 = KVRangeDescriptor.newBuilder()
.setId(kvRangeId2)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setVer(1)
.setBoundary(Boundary.newBuilder()
.setStartKey(ByteString.copyFromUtf8("n"))
@@ -121,6 +128,11 @@ public class RedundantRangeRemovalBalancerTest {
balancer.update(storeDescriptors);
BalanceResult command = balancer.balance();
+ // first returns AwaitBalance due to suspicion delay
+ assertEquals(command.type(), BalanceResultType.AwaitBalance);
+ // advance mock time beyond the max suspicion window (2s)
+ mockTime.set(3000L);
+ command = balancer.balance();
assertEquals(command.type(), BalanceResultType.BalanceNow);
ChangeConfigCommand changeConfigCommand = (ChangeConfigCommand) ((BalanceNow<?>) command).command;
@@ -137,6 +149,7 @@ public class RedundantRangeRemovalBalancerTest {
.setId(kvRangeId1)
.setRole(RaftNodeStatus.Leader)
.setVer(1)
+ .setState(State.StateType.Normal)
.setBoundary(Boundary.newBuilder()
.setStartKey(ByteString.copyFromUtf8("a"))
.setEndKey(ByteString.copyFromUtf8("m"))
@@ -150,6 +163,7 @@ public class RedundantRangeRemovalBalancerTest {
KVRangeDescriptor kvRangeDescriptor2 = KVRangeDescriptor.newBuilder()
.setId(kvRangeId2)
.setRole(RaftNodeStatus.Follower)
+ .setState(State.StateType.Normal)
.setVer(1)
.setBoundary(Boundary.newBuilder()
.setStartKey(ByteString.copyFromUtf8("n"))
@@ -187,6 +201,7 @@ public class RedundantRangeRemovalBalancerTest {
.setId(kvRangeId1)
.setVer(1)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setBoundary(boundary)
.setConfig(config)
.build();
@@ -194,6 +209,7 @@ public class RedundantRangeRemovalBalancerTest {
.setId(kvRangeId2)
.setVer(1)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setBoundary(boundary)
.setConfig(config)
.build();
@@ -209,7 +225,10 @@ public class RedundantRangeRemovalBalancerTest {
balancer.update(storeDescriptors);
BalanceResult result = balancer.balance();
-
+ // first returns AwaitBalance due to suspicion delay
+ assertEquals(result.type(), BalanceResultType.AwaitBalance);
+ mockTime.set(3000L);
+ result = balancer.balance();
assertEquals(result.type(), BalanceResultType.BalanceNow);
ChangeConfigCommand command = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
@@ -226,6 +245,7 @@ public class RedundantRangeRemovalBalancerTest {
KVRangeDescriptor kvRangeDescriptor = KVRangeDescriptor.newBuilder()
.setId(kvRangeId)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setVer(1)
.setBoundary(Boundary.newBuilder()
.setStartKey(ByteString.copyFromUtf8("a"))
@@ -259,6 +279,7 @@ public class RedundantRangeRemovalBalancerTest {
KVRangeDescriptor localRange = KVRangeDescriptor.newBuilder()
.setId(kvRangeId)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setVer(1)
.setBoundary(boundary)
.setConfig(ClusterConfig.newBuilder()
@@ -289,6 +310,10 @@ public class RedundantRangeRemovalBalancerTest {
balancer.update(Set.of(localStoreDesc, peerStoreDesc));
BalanceResult result = balancer.balance();
+ // first returns AwaitBalance due to suspicion delay
+ assertEquals(result.type(), BalanceResultType.AwaitBalance);
+ mockTime.set(3000L);
+ result = balancer.balance();
assertEquals(result.type(), BalanceResultType.BalanceNow);
ChangeConfigCommand cmd = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
@@ -311,6 +336,7 @@ public class RedundantRangeRemovalBalancerTest {
.setId(kvRangeId)
.setRole(RaftNodeStatus.Leader)
.setVer(1)
+ .setState(State.StateType.Normal)
.setBoundary(boundary)
.setConfig(ClusterConfig.newBuilder()
.addVoters(localStoreId)
@@ -321,6 +347,7 @@ public class RedundantRangeRemovalBalancerTest {
.setId(kvRangeId)
.setRole(RaftNodeStatus.Leader)
.setVer(1)
+ .setState(State.StateType.Normal)
.setBoundary(boundary)
.setConfig(ClusterConfig.newBuilder()
.addVoters(peerStoreId)
@@ -342,4 +369,49 @@ public class RedundantRangeRemovalBalancerTest {
BalanceResult result = balancer.balance();
assertSame(result.type(), BalanceResultType.NoNeedBalance);
}
-}
\ No newline at end of file
+
+ @Test
+ public void idConflictButVotersOverlapShouldNotDelete() {
+ String peerStoreId = "peer";
+ KVRangeId kvRangeId = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ Boundary boundary = Boundary.newBuilder()
+ .setStartKey(ByteString.copyFromUtf8("a"))
+ .setEndKey(ByteString.copyFromUtf8("z")).build();
+
+ KVRangeDescriptor localRange = KVRangeDescriptor.newBuilder()
+ .setId(kvRangeId)
+ .setRole(RaftNodeStatus.Leader)
+ .setVer(1)
+ .setState(State.StateType.Normal)
+ .setBoundary(boundary)
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters(localStoreId)
+ .addVoters("x").build())
+ .build();
+
+ KVRangeDescriptor peerRange = KVRangeDescriptor.newBuilder()
+ .setId(kvRangeId)
+ .setRole(RaftNodeStatus.Leader)
+ .setVer(1)
+ .setState(State.StateType.Normal)
+ .setBoundary(boundary)
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters(localStoreId)
+ .addVoters(peerStoreId).build())
+ .build();
+
+ KVRangeStoreDescriptor localStoreDesc = KVRangeStoreDescriptor.newBuilder()
+ .setId(localStoreId)
+ .addRanges(localRange)
+ .build();
+ KVRangeStoreDescriptor peerStoreDesc = KVRangeStoreDescriptor.newBuilder()
+ .setId(peerStoreId)
+ .addRanges(peerRange)
+ .build();
+
+ balancer.update(Set.of(localStoreDesc, peerStoreDesc));
+
+ BalanceResult result = balancer.balance();
+ assertSame(result.type(), BalanceResultType.NoNeedBalance);
+ }
+}
diff --git a/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/ReplicaCntBalancerTest.java b/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/ReplicaCntBalancerTest.java
index 346eedd3..1b5b7fc1 100644
--- a/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/ReplicaCntBalancerTest.java
+++ b/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/ReplicaCntBalancerTest.java
@@ -40,6 +40,7 @@ import org.apache.bifromq.basekv.proto.Boundary;
import org.apache.bifromq.basekv.proto.KVRangeDescriptor;
import org.apache.bifromq.basekv.proto.KVRangeId;
import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
+import org.apache.bifromq.basekv.proto.State;
import org.apache.bifromq.basekv.raft.proto.ClusterConfig;
import org.apache.bifromq.basekv.raft.proto.RaftNodeStatus;
import org.apache.bifromq.basekv.utils.EffectiveRoute;
@@ -69,6 +70,7 @@ public class ReplicaCntBalancerTest {
.setId(kvRangeId1)
.setRole(RaftNodeStatus.Leader)
.setVer(1)
+ .setState(State.StateType.Normal)
.setBoundary(Boundary.newBuilder().setEndKey(ByteString.copyFromUtf8("a")).build())
.setConfig(ClusterConfig.newBuilder()
.addVoters("s1")
@@ -82,6 +84,7 @@ public class ReplicaCntBalancerTest {
.setId(kvRangeId2)
.setRole(RaftNodeStatus.Leader)
.setVer(1)
+ .setState(State.StateType.Normal)
.setBoundary(Boundary.newBuilder().setStartKey(ByteString.copyFromUtf8("a")).build())
.setConfig(ClusterConfig.newBuilder()
.addVoters("s1")
@@ -127,6 +130,7 @@ public class ReplicaCntBalancerTest {
.setId(kvRangeId)
.setRole(RaftNodeStatus.Leader)
.setVer(1)
+ .setState(State.StateType.Normal)
.setBoundary(FULL_BOUNDARY)
.setConfig(ClusterConfig.newBuilder()
.addVoters("localStore")
@@ -163,16 +167,17 @@ public class ReplicaCntBalancerTest {
}
@Test
- public void balanceToRemoveVoter() {
+ public void balanceToRemoveLearner() {
KVRangeId kvRangeId = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
KVRangeDescriptor kvRangeDescriptor = KVRangeDescriptor.newBuilder()
.setId(kvRangeId)
.setRole(RaftNodeStatus.Leader)
.setVer(1)
+ .setState(State.StateType.Normal)
.setBoundary(FULL_BOUNDARY)
.setConfig(ClusterConfig.newBuilder()
.addVoters("localStore")
- .addVoters("remoteStore")
+ .addLearners("learnerStore")
.build())
.build();
@@ -190,63 +195,43 @@ public class ReplicaCntBalancerTest {
BalanceResult result = balancer.balance();
ChangeConfigCommand command = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
assertTrue(command.getVoters().contains("localStore"));
- assertFalse(command.getVoters().contains("remoteStore"));
assertTrue(command.getLearners().isEmpty());
}
@Test
- public void balanceToRemoveLearner() {
+ public void promoteLearnersToVoters() {
KVRangeId kvRangeId = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
- KVRangeDescriptor kvRangeDescriptor = KVRangeDescriptor.newBuilder()
+ KVRangeDescriptor leader = KVRangeDescriptor.newBuilder()
.setId(kvRangeId)
.setRole(RaftNodeStatus.Leader)
.setVer(1)
+ .setState(State.StateType.Normal)
.setBoundary(FULL_BOUNDARY)
.setConfig(ClusterConfig.newBuilder()
.addVoters("localStore")
- .addLearners("learnerStore")
+ .addLearners("remoteStore")
.build())
.build();
-
- KVRangeStoreDescriptor storeDescriptor = KVRangeStoreDescriptor.newBuilder()
- .setId("localStore")
- .addRanges(kvRangeDescriptor)
- .putStatistics("cpu.usage", 0.5)
- .build();
-
- Set<KVRangeStoreDescriptor> storeDescriptors = new HashSet<>();
- storeDescriptors.add(storeDescriptor);
-
- balancer.update(storeDescriptors);
-
- BalanceResult result = balancer.balance();
- ChangeConfigCommand command = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
- assertTrue(command.getVoters().contains("localStore"));
- assertTrue(command.getLearners().isEmpty());
- }
-
- @Test
- public void nothingChanged() {
- KVRangeId kvRangeId = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
- KVRangeDescriptor kvRangeDescriptor = KVRangeDescriptor.newBuilder()
+ KVRangeDescriptor learner = KVRangeDescriptor.newBuilder()
.setId(kvRangeId)
- .setRole(RaftNodeStatus.Leader)
+ .setRole(RaftNodeStatus.Follower)
.setVer(1)
+ .setState(State.StateType.Normal)
.setBoundary(FULL_BOUNDARY)
.setConfig(ClusterConfig.newBuilder()
.addVoters("localStore")
- .addLearners("learnerStore")
+ .addLearners("remoteStore")
.build())
.build();
KVRangeStoreDescriptor storeDescriptor = KVRangeStoreDescriptor.newBuilder()
.setId("localStore")
- .addRanges(kvRangeDescriptor)
+ .addRanges(leader)
.putStatistics("cpu.usage", 0.5)
.build();
KVRangeStoreDescriptor learnerStoreDescriptor = KVRangeStoreDescriptor.newBuilder()
- .setId("learnerStore")
- .addRanges(kvRangeDescriptor)
+ .setId("remoteStore")
+ .addRanges(learner)
.putStatistics("cpu.usage", 0.5)
.build();
@@ -255,8 +240,10 @@ public class ReplicaCntBalancerTest {
storeDescriptors.add(learnerStoreDescriptor);
balancer.update(storeDescriptors);
-
- assertSame(balancer.balance().type(), BalanceResultType.NoNeedBalance);
+ ChangeConfigCommand command = (ChangeConfigCommand) ((BalanceNow<?>) balancer.balance()).command;
+ assertTrue(command.getVoters().contains("localStore"));
+ assertTrue(command.getVoters().contains("remoteStore"));
+ assertTrue(command.getLearners().isEmpty());
}
@Test
@@ -267,6 +254,7 @@ public class ReplicaCntBalancerTest {
.setId(kvRangeId)
.setRole(RaftNodeStatus.Leader)
.setVer(1)
+ .setState(State.StateType.Normal)
.setBoundary(FULL_BOUNDARY)
.setConfig(ClusterConfig.newBuilder()
.addVoters("localStore")
@@ -318,6 +306,7 @@ public class ReplicaCntBalancerTest {
KVRangeDescriptor kvRangeDescriptor1 = KVRangeDescriptor.newBuilder()
.setId(kvRangeId1)
.setVer(1)
+ .setState(State.StateType.Normal)
.setRole(RaftNodeStatus.Leader)
.setBoundary(Boundary.newBuilder().setEndKey(ByteString.copyFromUtf8("z")).build())
.setConfig(ClusterConfig.newBuilder()
@@ -329,6 +318,7 @@ public class ReplicaCntBalancerTest {
.setId(kvRangeId2)
.setVer(2)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setBoundary(Boundary.newBuilder().setStartKey(ByteString.copyFromUtf8("z")).build())
.setConfig(ClusterConfig.newBuilder()
.addVoters("localStore")
@@ -364,6 +354,7 @@ public class ReplicaCntBalancerTest {
.setId(kvRangeId1)
.setVer(1)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setBoundary(Boundary.newBuilder().setEndKey(ByteString.copyFromUtf8("a")).build())
.setConfig(ClusterConfig.newBuilder()
.addVoters("s1")
@@ -380,6 +371,7 @@ public class ReplicaCntBalancerTest {
.setId(kvRangeId2)
.setVer(2)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setBoundary(Boundary.newBuilder().setStartKey(ByteString.copyFromUtf8("a"))
.setEndKey(ByteString.copyFromUtf8("z")).build())
.setConfig(ClusterConfig.newBuilder()
@@ -398,6 +390,7 @@ public class ReplicaCntBalancerTest {
.setId(kvRangeId3)
.setVer(2)
.setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
.setBoundary(Boundary.newBuilder().setStartKey(ByteString.copyFromUtf8("z")).build())
.setConfig(ClusterConfig.newBuilder()
.addVoters("s3")
@@ -432,6 +425,7 @@ public class ReplicaCntBalancerTest {
.setId(kvRangeId)
.setRole(RaftNodeStatus.Leader)
.setVer(1)
+ .setState(State.StateType.Normal)
.setBoundary(FULL_BOUNDARY)
.setConfig(ClusterConfig.newBuilder()
.addVoters("localStore")
@@ -464,4 +458,502 @@ public class ReplicaCntBalancerTest {
assertTrue(balancer.verify(layout, allStoreDescriptors));
}
+
+ @Test
+ public void removeDeadVoterAndBackfillEvenIfCountEqualsExpected() {
+ // live: s1, s2, s3ï¼›expected voters=3
+ // range current voters = [s1, ghost, s2]
+ KVRangeId kvRangeId = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeDescriptor range = KVRangeDescriptor.newBuilder()
+ .setId(kvRangeId)
+ .setRole(RaftNodeStatus.Leader)
+ .setVer(1)
+ .setState(State.StateType.Normal)
+ .setBoundary(FULL_BOUNDARY)
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters("s1")
+ .addVoters("ghost")
+ .addVoters("s2")
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor s1 = KVRangeStoreDescriptor.newBuilder().setId("s1").addRanges(range).build();
+ KVRangeStoreDescriptor s2 = KVRangeStoreDescriptor.newBuilder().setId("s2").build();
+ KVRangeStoreDescriptor s3 = KVRangeStoreDescriptor.newBuilder().setId("s3").build();
+
+ Set<KVRangeStoreDescriptor> stores = new HashSet<>();
+ stores.add(s1);
+ stores.add(s2);
+ stores.add(s3);
+
+ // votersPerRange=3,learnersPerRange=0
+ balancer = new ReplicaCntBalancer("testCluster", "s1", 3, 0);
+ balancer.update(stores);
+
+ BalanceResult result = balancer.balance();
+ assertSame(result.type(), BalanceResultType.BalanceNow);
+ ChangeConfigCommand cmd = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
+
+ // expected:ghost removed s3 added
+ assertEquals(cmd.getKvRangeId(), kvRangeId);
+ assertTrue(cmd.getVoters().contains("s1"));
+ assertTrue(cmd.getVoters().contains("s2"));
+ assertTrue(cmd.getVoters().contains("s3"));
+ assertFalse(cmd.getVoters().contains("ghost"));
+ assertTrue(cmd.getLearners().isEmpty());
+ }
+
+ @Test
+ public void abortWhenConfigChangeInProgress_nextFieldsPresent() {
+ KVRangeId kvRangeId = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ ClusterConfig cfgWithNext = ClusterConfig.newBuilder()
+ .addVoters("localStore")
+ .addNextVoters("someone")
+ .build();
+
+ KVRangeDescriptor range = KVRangeDescriptor.newBuilder()
+ .setId(kvRangeId)
+ .setRole(RaftNodeStatus.Leader)
+ .setVer(1)
+ .setBoundary(FULL_BOUNDARY)
+ .setConfig(cfgWithNext)
+ .build();
+
+ KVRangeStoreDescriptor local = KVRangeStoreDescriptor.newBuilder()
+ .setId("localStore")
+ .addRanges(range)
+ .build();
+
+ Set<KVRangeStoreDescriptor> stores = new HashSet<>();
+ stores.add(local);
+
+ balancer = new ReplicaCntBalancer("testCluster", "localStore", 1, 0);
+ balancer.update(stores);
+
+ assertSame(balancer.balance().type(), BalanceResultType.NoNeedBalance);
+ }
+
+ @Test
+ public void learnersMinusOneUsesLiveMinusVotersAndSanitizes() {
+ // expectedLearners = -1 => learners = live - votersï¼›
+ balancer = new ReplicaCntBalancer("testCluster", "s1", 1, -1);
+
+ KVRangeId kvRangeId = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeDescriptor range = KVRangeDescriptor.newBuilder()
+ .setId(kvRangeId)
+ .setRole(RaftNodeStatus.Leader)
+ .setVer(1)
+ .setState(State.StateType.Normal)
+ .setBoundary(FULL_BOUNDARY)
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters("s1")
+ .addLearners("ghostLearner")
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor s1 = KVRangeStoreDescriptor.newBuilder().setId("s1").addRanges(range).build();
+ KVRangeStoreDescriptor s2 = KVRangeStoreDescriptor.newBuilder().setId("s2").build();
+ KVRangeStoreDescriptor s3 = KVRangeStoreDescriptor.newBuilder().setId("s3").build();
+
+ Set<KVRangeStoreDescriptor> stores = new HashSet<>();
+ stores.add(s1);
+ stores.add(s2);
+ stores.add(s3);
+
+ balancer.update(stores);
+
+ BalanceResult result = balancer.balance();
+ assertSame(result.type(), BalanceResultType.BalanceNow);
+ ChangeConfigCommand cmd = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
+
+ // expected:learners = live - voters = {s2, s3};ghostLearner removed
+ assertTrue(cmd.getVoters().contains("s1"));
+ assertFalse(cmd.getLearners().contains("ghostLearner"));
+ assertTrue(cmd.getLearners().contains("s2"));
+ assertTrue(cmd.getLearners().contains("s3"));
+ assertEquals(cmd.getLearners().size(), 2);
+ }
+
+ @Test
+ public void skipWhenCapacityInsufficientAndHasDeadVoter() {
+ // expected voters=3,live voters=S1,S2, S3(dead)
+ ReplicaCntBalancer balancer = new ReplicaCntBalancer("testCluster", "s1", 3, 0);
+
+ KVRangeId rid = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeDescriptor range = KVRangeDescriptor.newBuilder()
+ .setId(rid)
+ .setRole(RaftNodeStatus.Leader)
+ .setVer(1)
+ .setBoundary(org.apache.bifromq.basekv.utils.BoundaryUtil.FULL_BOUNDARY)
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters("s1")
+ .addVoters("s2")
+ .addVoters("deadS3") // dead
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor s1 = KVRangeStoreDescriptor.newBuilder().setId("s1").addRanges(range).build();
+ KVRangeStoreDescriptor s2 = KVRangeStoreDescriptor.newBuilder().setId("s2").build();
+ Set<KVRangeStoreDescriptor> landscape = new HashSet<>();
+ landscape.add(s1);
+ landscape.add(s2);
+
+ balancer.update(landscape);
+
+ assertSame(balancer.balance().type(), BalanceResultType.NoNeedBalance);
+ }
+
+ @Test
+ public void learnersMinusOnePreferPromoteLearnersToFillVoters() {
+ // expected: voters=3, learners=-1
+ balancer = new ReplicaCntBalancer("testCluster", "s1", 3, -1);
+
+ KVRangeId rid = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeDescriptor range = KVRangeDescriptor.newBuilder()
+ .setId(rid)
+ .setRole(RaftNodeStatus.Leader)
+ .setVer(1)
+ .setState(State.StateType.Normal)
+ .setBoundary(FULL_BOUNDARY)
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters("s1")
+ .addLearners("s2")
+ .addLearners("s3")
+ .addLearners("s4")
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor s1 = KVRangeStoreDescriptor.newBuilder().setId("s1").addRanges(range).build();
+ KVRangeStoreDescriptor s2 = KVRangeStoreDescriptor.newBuilder().setId("s2").build();
+ KVRangeStoreDescriptor s3 = KVRangeStoreDescriptor.newBuilder().setId("s3").build();
+ KVRangeStoreDescriptor s4 = KVRangeStoreDescriptor.newBuilder().setId("s4").build();
+
+ balancer.update(Set.of(s1, s2, s3, s4));
+
+ BalanceResult result = balancer.balance();
+ assertSame(result.type(), BalanceResultType.BalanceNow);
+ ChangeConfigCommand cmd = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
+
+ // voters should be s1 + two from {s2,s3,s4}
+ assertTrue(cmd.getVoters().contains("s1"));
+ assertEquals(cmd.getVoters().size(), 3);
+ // after promotion, learners should be live - voters = the remaining one
+ assertEquals(cmd.getLearners().size(), 1);
+ Set<String> all = Set.of("s1", "s2", "s3", "s4");
+ Set<String> union = new HashSet<>(cmd.getVoters());
+ union.addAll(cmd.getLearners());
+ assertEquals(union, all);
+ }
+
+ @Test
+ public void noChangeWhenLiveLessThanExpectedAndNoDeadVoter() {
+ // expected voters=3, live={s1,s2}, voters={s1,s2}
+ balancer = new ReplicaCntBalancer("testCluster", "s1", 3, 0);
+
+ KVRangeId rid = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeDescriptor range = KVRangeDescriptor.newBuilder()
+ .setId(rid)
+ .setRole(RaftNodeStatus.Leader)
+ .setVer(1)
+ .setState(State.StateType.Normal)
+ .setBoundary(FULL_BOUNDARY)
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters("s1")
+ .addVoters("s2")
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor s1 = KVRangeStoreDescriptor.newBuilder().setId("s1").addRanges(range).build();
+ KVRangeStoreDescriptor s2 = KVRangeStoreDescriptor.newBuilder().setId("s2").build();
+
+ balancer.update(Set.of(s1, s2));
+ assertSame(balancer.balance().type(), BalanceResultType.NoNeedBalance);
+ }
+
+ @Test
+ public void neverRemoveLeaderWhenShrinkingVoters() {
+ // expected voters=3, voters currently 4 (leader must stay)
+ balancer = new ReplicaCntBalancer("testCluster", "leader", 3, 0);
+
+ KVRangeId rid = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeDescriptor range = KVRangeDescriptor.newBuilder()
+ .setId(rid)
+ .setRole(RaftNodeStatus.Leader)
+ .setVer(1)
+ .setState(State.StateType.Normal)
+ .setBoundary(FULL_BOUNDARY)
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters("leader")
+ .addVoters("s2")
+ .addVoters("s3")
+ .addVoters("s4")
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor leader = KVRangeStoreDescriptor.newBuilder().setId("leader").addRanges(range).build();
+ KVRangeStoreDescriptor s2 = KVRangeStoreDescriptor.newBuilder().setId("s2").build();
+ KVRangeStoreDescriptor s3 = KVRangeStoreDescriptor.newBuilder().setId("s3").build();
+ KVRangeStoreDescriptor s4 = KVRangeStoreDescriptor.newBuilder().setId("s4").build();
+
+ balancer.update(Set.of(leader, s2, s3, s4));
+
+ BalanceResult result = balancer.balance();
+ assertSame(result.type(), BalanceResultType.BalanceNow);
+ ChangeConfigCommand cmd = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
+
+ assertTrue(cmd.getVoters().contains("leader"));
+ assertEquals(cmd.getVoters().size(), 3);
+ }
+
+ @Test
+ public void balanceVoterCountNoopWhenSpreadWithinOne() {
+ // two stores, two ranges: counts differ by at most 1 -> no rebalance
+ balancer = new ReplicaCntBalancer("testCluster", "s1", 1, 0);
+
+ KVRangeId r1 = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeDescriptor d1 = KVRangeDescriptor.newBuilder()
+ .setId(r1).setVer(1).setRole(RaftNodeStatus.Leader)
+ .setBoundary(Boundary.newBuilder().setEndKey(ByteString.copyFromUtf8("m")).build())
+ .setConfig(ClusterConfig.newBuilder().addVoters("s1").build())
+ .build();
+
+ KVRangeId r2 = KVRangeId.newBuilder().setEpoch(1).setId(2).build();
+ KVRangeDescriptor d2 = KVRangeDescriptor.newBuilder()
+ .setId(r2).setVer(1).setRole(RaftNodeStatus.Leader)
+ .setBoundary(Boundary.newBuilder().setStartKey(ByteString.copyFromUtf8("m")).build())
+ .setConfig(ClusterConfig.newBuilder().addVoters("s2").build())
+ .build();
+
+ KVRangeStoreDescriptor s1 = KVRangeStoreDescriptor.newBuilder().setId("s1").addRanges(d1).build();
+ KVRangeStoreDescriptor s2 = KVRangeStoreDescriptor.newBuilder().setId("s2").addRanges(d2).build();
+
+ balancer.update(Set.of(s1, s2));
+ assertSame(balancer.balance().type(), BalanceResultType.NoNeedBalance);
+ }
+
+ @Test
+ public void fixedLearnerCountRemovesDeadAndBackfills() {
+ // expected learners=2; current learners={deadL, s2}; live={s1,s2,s3,s4}
+ balancer = new ReplicaCntBalancer("testCluster", "s1", 1, 2);
+
+ KVRangeId rid = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeDescriptor range = KVRangeDescriptor.newBuilder()
+ .setId(rid)
+ .setRole(RaftNodeStatus.Leader)
+ .setVer(1)
+ .setState(State.StateType.Normal)
+ .setBoundary(FULL_BOUNDARY)
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters("s1")
+ .addLearners("deadL")
+ .addLearners("s2")
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor s1 = KVRangeStoreDescriptor.newBuilder().setId("s1").addRanges(range).build();
+ KVRangeStoreDescriptor s2 = KVRangeStoreDescriptor.newBuilder().setId("s2").build();
+ KVRangeStoreDescriptor s3 = KVRangeStoreDescriptor.newBuilder().setId("s3").build();
+ KVRangeStoreDescriptor s4 = KVRangeStoreDescriptor.newBuilder().setId("s4").build();
+
+ balancer.update(Set.of(s1, s2, s3, s4));
+
+ BalanceResult result = balancer.balance();
+ assertSame(result.type(), BalanceResultType.BalanceNow);
+ ChangeConfigCommand cmd = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
+
+ assertTrue(cmd.getVoters().contains("s1"));
+ assertEquals(cmd.getLearners().size(), 2);
+ assertTrue(cmd.getLearners().contains("s2"));
+ assertFalse(cmd.getLearners().contains("deadL"));
+ }
+
+ @Test
+ public void zeroLearnersTargetClearsLearners() {
+ // expected learners=0
+ balancer = new ReplicaCntBalancer("testCluster", "s1", 1, 0);
+
+ KVRangeId rid = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeDescriptor range = KVRangeDescriptor.newBuilder()
+ .setId(rid)
+ .setRole(RaftNodeStatus.Leader)
+ .setVer(1)
+ .setState(State.StateType.Normal)
+ .setBoundary(FULL_BOUNDARY)
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters("s1")
+ .addLearners("s2")
+ .addLearners("s3")
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor s1 = KVRangeStoreDescriptor.newBuilder().setId("s1").addRanges(range).build();
+ KVRangeStoreDescriptor s2 = KVRangeStoreDescriptor.newBuilder().setId("s2").build();
+ KVRangeStoreDescriptor s3 = KVRangeStoreDescriptor.newBuilder().setId("s3").build();
+
+ balancer.update(Set.of(s1, s2, s3));
+ BalanceResult result = balancer.balance();
+ assertSame(result.type(), BalanceResultType.BalanceNow);
+ ChangeConfigCommand cmd = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
+ assertTrue(cmd.getLearners().isEmpty());
+ assertTrue(cmd.getVoters().contains("s1"));
+ }
+
+ @Test
+ public void learnersMinusOneWithAllLiveAsVotersMakesLearnersEmpty() {
+ balancer = new ReplicaCntBalancer("testCluster", "s1", 3, -1);
+
+ KVRangeId rid = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeDescriptor range = KVRangeDescriptor.newBuilder()
+ .setId(rid)
+ .setRole(RaftNodeStatus.Leader)
+ .setVer(1)
+ .setState(State.StateType.Normal)
+ .setBoundary(FULL_BOUNDARY)
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters("s1")
+ .addVoters("s2")
+ .addVoters("s3")
+ .addLearners("ghost") // should be sanitized away
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor s1 = KVRangeStoreDescriptor.newBuilder().setId("s1").addRanges(range).build();
+ KVRangeStoreDescriptor s2 = KVRangeStoreDescriptor.newBuilder().setId("s2").build();
+ KVRangeStoreDescriptor s3 = KVRangeStoreDescriptor.newBuilder().setId("s3").build();
+
+ balancer.update(Set.of(s1, s2, s3));
+ BalanceResult result = balancer.balance();
+ assertSame(result.type(), BalanceResultType.BalanceNow);
+ ChangeConfigCommand cmd = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
+
+ assertEquals(cmd.getVoters(), Set.of("s1", "s2", "s3"));
+ assertTrue(cmd.getLearners().isEmpty());
+ }
+
+ @Test
+ public void balanceVoterCountPrefersZeroCountStoreFirst() {
+ KVRangeId r1 = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeId r2 = KVRangeId.newBuilder().setEpoch(1).setId(2).build();
+ KVRangeId r3 = KVRangeId.newBuilder().setEpoch(1).setId(3).build();
+ KVRangeDescriptor d1 = KVRangeDescriptor.newBuilder()
+ .setId(r1)
+ .setVer(1)
+ .setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
+ .setBoundary(Boundary.newBuilder().setEndKey(ByteString.copyFromUtf8("m")).build())
+ .setConfig(ClusterConfig.newBuilder().addVoters("sA").build())
+ .build();
+ KVRangeDescriptor d2 = KVRangeDescriptor.newBuilder()
+ .setId(r2)
+ .setVer(1)
+ .setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
+ .setBoundary(Boundary.newBuilder()
+ .setStartKey(ByteString.copyFromUtf8("m"))
+ .setEndKey(ByteString.copyFromUtf8("z"))
+ .build())
+ .setConfig(ClusterConfig.newBuilder().addVoters("sA").build())
+ .build();
+
+ KVRangeStoreDescriptor sA = KVRangeStoreDescriptor.newBuilder().setId("sA").addRanges(d1).addRanges(d2).build();
+ KVRangeStoreDescriptor sB = KVRangeStoreDescriptor.newBuilder().setId("sB")
+ .addRanges(KVRangeDescriptor.newBuilder()
+ .setId(r3)
+ .setVer(1)
+ .setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
+ .setBoundary(Boundary.newBuilder()
+ .setStartKey(ByteString.copyFromUtf8("z"))
+ .build())
+ .setConfig(ClusterConfig.newBuilder().addVoters("sB").build())
+ .build())
+ .build();
+ KVRangeStoreDescriptor sC = KVRangeStoreDescriptor.newBuilder().setId("sC").build();
+
+ balancer = new ReplicaCntBalancer("testCluster", "sA", 1, 0);
+ balancer.update(Set.of(sA, sB, sC));
+
+ BalanceResult result = balancer.balance();
+ assertSame(result.type(), BalanceResultType.BalanceNow);
+ ChangeConfigCommand cmd = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
+
+ assertTrue(cmd.getVoters().contains("sC"));
+ assertFalse(cmd.getVoters().contains("sA"));
+ }
+
+ @Test
+ public void balanceVoterCountDoesOnlyOneChangePerRound() {
+ KVRangeId r1 = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeId r2 = KVRangeId.newBuilder().setEpoch(1).setId(2).build();
+ KVRangeDescriptor d1 = KVRangeDescriptor.newBuilder()
+ .setId(r1)
+ .setVer(1)
+ .setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
+ .setBoundary(Boundary.newBuilder().setEndKey(ByteString.copyFromUtf8("m")).build())
+ .setConfig(ClusterConfig.newBuilder().addVoters("sA").build())
+ .build();
+ KVRangeDescriptor d2 = KVRangeDescriptor.newBuilder()
+ .setId(r2)
+ .setVer(1)
+ .setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
+ .setBoundary(Boundary.newBuilder().setStartKey(ByteString.copyFromUtf8("m")).build())
+ .setConfig(ClusterConfig.newBuilder().addVoters("sA").build())
+ .build();
+
+ KVRangeStoreDescriptor sA = KVRangeStoreDescriptor.newBuilder().setId("sA").addRanges(d1).addRanges(d2).build();
+ KVRangeStoreDescriptor sB = KVRangeStoreDescriptor.newBuilder().setId("sB").build();
+ KVRangeStoreDescriptor sC = KVRangeStoreDescriptor.newBuilder().setId("sC").build();
+
+ balancer = new ReplicaCntBalancer("testCluster", "sA", 1, 0);
+ balancer.update(Set.of(sA, sB, sC));
+
+ BalanceResult result = balancer.balance();
+ assertSame(result.type(), BalanceResultType.BalanceNow);
+ ChangeConfigCommand cmd = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
+ assertTrue(cmd.getKvRangeId().equals(r1) || cmd.getKvRangeId().equals(r2));
+ }
+
+ @Test
+ public void balanceVoterCountSkipsTargetsAlreadyInVotersOrLearners() {
+ KVRangeId r1 = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeDescriptor d1 = KVRangeDescriptor.newBuilder()
+ .setId(r1)
+ .setVer(1)
+ .setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
+ .setBoundary(Boundary.newBuilder().setEndKey(ByteString.copyFromUtf8("z")).build())
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters("sA")
+ .addLearners("sB")
+ .build())
+ .build();
+
+ KVRangeDescriptor d2 = KVRangeDescriptor.newBuilder()
+ .setId(KVRangeId.newBuilder().setEpoch(1).setId(2).build())
+ .setVer(1)
+ .setRole(RaftNodeStatus.Leader)
+ .setState(State.StateType.Normal)
+ .setBoundary(Boundary.newBuilder().setStartKey(ByteString.copyFromUtf8("z")).build())
+ .setConfig(ClusterConfig.newBuilder()
+ .addVoters("sA")
+ .addLearners("sB")
+ .build())
+ .build();
+
+ KVRangeStoreDescriptor sA = KVRangeStoreDescriptor.newBuilder().setId("sA").addRanges(d1).addRanges(d2).build();
+ KVRangeStoreDescriptor sB = KVRangeStoreDescriptor.newBuilder().setId("sB").build();
+ KVRangeStoreDescriptor sC = KVRangeStoreDescriptor.newBuilder().setId("sC").build();
+
+ balancer = new ReplicaCntBalancer("testCluster", "sA", 1, 1);
+ balancer.update(Set.of(sA, sB, sC));
+
+ BalanceResult result = balancer.balance();
+ assertSame(result.type(), BalanceResultType.BalanceNow);
+ ChangeConfigCommand cmd = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
+
+ assertTrue(cmd.getVoters().contains("sC"));
+ assertFalse(cmd.getVoters().contains("sB"));
+ }
}
diff --git a/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/UnreachableReplicaRemovalBalancerTest.java b/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/UnreachableReplicaRemovalBalancerTest.java
index 752ebbcf..dc2ccb35 100644
--- a/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/UnreachableReplicaRemovalBalancerTest.java
+++ b/base-kv/base-kv-store-balance-controller/src/test/java/org/apache/bifromq/basekv/balance/impl/UnreachableReplicaRemovalBalancerTest.java
@@ -25,6 +25,12 @@ import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertSame;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
import org.apache.bifromq.basekv.balance.BalanceNow;
import org.apache.bifromq.basekv.balance.BalanceResult;
import org.apache.bifromq.basekv.balance.BalanceResultType;
@@ -36,22 +42,16 @@ import org.apache.bifromq.basekv.proto.State;
import org.apache.bifromq.basekv.raft.proto.ClusterConfig;
import org.apache.bifromq.basekv.raft.proto.RaftNodeStatus;
import org.apache.bifromq.basekv.raft.proto.RaftNodeSyncState;
-import java.time.Duration;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Supplier;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class UnreachableReplicaRemovalBalancerTest {
- private UnreachableReplicaRemovalBalancer balancer;
- private Supplier<Long> mockTimeSource;
private final String localStoreId = "localStore";
private final String peerStoreId = "peerStore";
private final KVRangeId rangeId = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ private UnreachableReplicaRemovalBalancer balancer;
+ private Supplier<Long> mockTimeSource;
@BeforeMethod
public void setUp() {
@@ -61,6 +61,36 @@ public class UnreachableReplicaRemovalBalancerTest {
new UnreachableReplicaRemovalBalancer("clusterId", localStoreId, Duration.ofSeconds(15), mockTimeSource);
}
+ @Test
+ public void noChangeWhenLocalStoreMissingInitially() {
+ KVRangeStoreDescriptor peerStoreDescriptor = createStoreDescriptor(peerStoreId);
+ balancer.update(Set.of(peerStoreDescriptor));
+ assertSame(balancer.balance().type(), BalanceResultType.NoNeedBalance);
+ }
+
+ @Test
+ public void noNPEWhenLocalStoreDisappearsAfterBeingLeader() {
+ KVRangeStoreDescriptor localStoreDescriptor = createStoreDescriptor(
+ localStoreId,
+ createRangeDescriptor(
+ rangeId,
+ RaftNodeStatus.Leader,
+ Map.of(localStoreId, RaftNodeSyncState.Replicating, peerStoreId, RaftNodeSyncState.Probing),
+ Set.of(localStoreId, peerStoreId),
+ Set.of()
+ )
+ );
+ KVRangeStoreDescriptor peerStoreDescriptor = createStoreDescriptor(peerStoreId);
+
+ when(mockTimeSource.get()).thenReturn(System.currentTimeMillis());
+ balancer.update(Set.of(localStoreDescriptor, peerStoreDescriptor));
+
+ when(mockTimeSource.get()).thenReturn(System.currentTimeMillis() + 16000);
+ balancer.update(Set.of(peerStoreDescriptor));
+
+ assertSame(balancer.balance().type(), BalanceResultType.NoNeedBalance);
+ }
+
@Test
public void noChangeWhenAllReplicasAreReachable() {
KVRangeStoreDescriptor storeDescriptor = createStoreDescriptor(
@@ -73,7 +103,6 @@ public class UnreachableReplicaRemovalBalancerTest {
balancer.update(Set.of(storeDescriptor));
-
assertSame(balancer.balance().type(), BalanceResultType.NoNeedBalance);
}
@@ -101,7 +130,7 @@ public class UnreachableReplicaRemovalBalancerTest {
// Verify that the unhealthy replica is scheduled for removal
assertEquals(localStoreId, command.getToStore());
assertEquals(rangeId, command.getKvRangeId());
- assertEquals(5, command.getExpectedVer());
+ assertEquals(command.getExpectedVer(), 5);
assertFalse(command.getVoters().contains(peerStoreId));
}
@@ -129,11 +158,10 @@ public class UnreachableReplicaRemovalBalancerTest {
// Verify that the unhealthy replica is scheduled for removal
assertEquals(localStoreId, command.getToStore());
assertEquals(rangeId, command.getKvRangeId());
- assertEquals(5, command.getExpectedVer());
+ assertEquals(command.getExpectedVer(), 5);
assertFalse(command.getLearners().contains(peerStoreId));
}
-
@Test
public void noCommandIfReplicaReachableAgain() {
KVRangeStoreDescriptor localStoreDescriptor = createStoreDescriptor(
@@ -177,7 +205,6 @@ public class UnreachableReplicaRemovalBalancerTest {
Set.of(localStoreId), Set.of(peerStoreId))
);
-
balancer.update(Set.of(storeDescriptor, peerStoreDescriptor));
// Simulate a leader change
diff --git a/base-kv/base-kv-store-balance-spi/src/main/java/org/apache/bifromq/basekv/balance/StoreBalancer.java b/base-kv/base-kv-store-balance-spi/src/main/java/org/apache/bifromq/basekv/balance/StoreBalancer.java
index c29a0783..13d6af6a 100644
--- a/base-kv/base-kv-store-balance-spi/src/main/java/org/apache/bifromq/basekv/balance/StoreBalancer.java
+++ b/base-kv/base-kv-store-balance-spi/src/main/java/org/apache/bifromq/basekv/balance/StoreBalancer.java
@@ -40,7 +40,8 @@ public abstract class StoreBalancer {
* @param localStoreId the id of the store which the balancer is responsible for
*/
public StoreBalancer(String clusterId, String localStoreId) {
- this.log = MDCLogger.getLogger("balancer.logger", "clusterId", clusterId, "storeId", localStoreId);
+ this.log = MDCLogger.getLogger("balancer.logger",
+ "clusterId", clusterId, "storeId", localStoreId, "balancer", this.getClass().getSimpleName());
this.clusterId = clusterId;
this.localStoreId = localStoreId;
}
diff --git a/base-kv/base-kv-store-balance-spi/src/main/java/org/apache/bifromq/basekv/balance/command/RecoveryCommand.java b/base-kv/base-kv-store-balance-spi/src/main/java/org/apache/bifromq/basekv/balance/command/RecoveryCommand.java
index 2c36c3f3..99fc0701 100644
--- a/base-kv/base-kv-store-balance-spi/src/main/java/org/apache/bifromq/basekv/balance/command/RecoveryCommand.java
+++ b/base-kv/base-kv-store-balance-spi/src/main/java/org/apache/bifromq/basekv/balance/command/RecoveryCommand.java
@@ -22,6 +22,7 @@ package org.apache.bifromq.basekv.balance.command;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
+import org.apache.bifromq.basekv.utils.KVRangeIdUtil;
@Getter
@Setter
@@ -35,6 +36,7 @@ public class RecoveryCommand extends BalanceCommand {
@Override
public String toString() {
- return String.format("RecoveryCommand{toStore=%s}", getToStore());
+ return String.format("RecoveryCommand{toStore=%s, kvRangeId=%s}",
+ getToStore(), KVRangeIdUtil.toString(getKvRangeId()));
}
}
diff --git a/base-kv/base-kv-store-balance-spi/src/main/java/org/apache/bifromq/basekv/balance/util/CommandUtil.java b/base-kv/base-kv-store-balance-spi/src/main/java/org/apache/bifromq/basekv/balance/util/CommandUtil.java
index 125ff420..dadf83ba 100644
--- a/base-kv/base-kv-store-balance-spi/src/main/java/org/apache/bifromq/basekv/balance/util/CommandUtil.java
+++ b/base-kv/base-kv-store-balance-spi/src/main/java/org/apache/bifromq/basekv/balance/util/CommandUtil.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basekv.balance.util;
@@ -23,8 +23,14 @@ import static org.apache.bifromq.basekv.utils.BoundaryUtil.inRange;
import static org.apache.bifromq.basekv.utils.BoundaryUtil.startKey;
import static org.apache.bifromq.basekv.utils.BoundaryUtil.toBoundary;
-import org.apache.bifromq.basekv.balance.BalanceNow;
-import org.apache.bifromq.basekv.balance.BalanceResult;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NavigableMap;
+import java.util.Set;
import org.apache.bifromq.basekv.balance.command.BalanceCommand;
import org.apache.bifromq.basekv.balance.command.BootstrapCommand;
import org.apache.bifromq.basekv.balance.command.ChangeConfigCommand;
@@ -37,14 +43,6 @@ import org.apache.bifromq.basekv.utils.BoundaryUtil;
import org.apache.bifromq.basekv.utils.EffectiveRoute;
import org.apache.bifromq.basekv.utils.KVRangeIdUtil;
import org.apache.bifromq.basekv.utils.LeaderRange;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.NavigableMap;
-import java.util.Set;
/**
* Utility class for generating balance commands.
@@ -57,24 +55,24 @@ public class CommandUtil {
* @param rangeDescriptor the range descriptor of the range which the balancer is responsible for
* @return the generated ChangeConfigCommand
*/
- public static BalanceResult quit(String localStoreId, KVRangeDescriptor rangeDescriptor) {
+ public static BalanceCommand quit(String localStoreId, KVRangeDescriptor rangeDescriptor) {
ClusterConfig config = rangeDescriptor.getConfig();
if (config.getVotersCount() > 1 || config.getLearnersCount() > 0) {
- return BalanceNow.of(ChangeConfigCommand.builder()
+ return ChangeConfigCommand.builder()
.toStore(localStoreId)
.kvRangeId(rangeDescriptor.getId())
.expectedVer(rangeDescriptor.getVer())
.voters(Set.of(localStoreId))
.learners(Collections.emptySet())
- .build());
+ .build();
} else {
- return BalanceNow.of(ChangeConfigCommand.builder()
+ return ChangeConfigCommand.builder()
.toStore(localStoreId)
.kvRangeId(rangeDescriptor.getId())
.expectedVer(rangeDescriptor.getVer())
.voters(Collections.emptySet())
.learners(Collections.emptySet())
- .build());
+ .build();
}
}
diff --git a/base-kv/base-kv-store-balance-spi/src/test/java/org/apache/bifromq/basekv/balance/util/CommandUtilTest.java b/base-kv/base-kv-store-balance-spi/src/test/java/org/apache/bifromq/basekv/balance/util/CommandUtilTest.java
index 2b06a80a..ae41eafa 100644
--- a/base-kv/base-kv-store-balance-spi/src/test/java/org/apache/bifromq/basekv/balance/util/CommandUtilTest.java
+++ b/base-kv/base-kv-store-balance-spi/src/test/java/org/apache/bifromq/basekv/balance/util/CommandUtilTest.java
@@ -19,17 +19,19 @@
package org.apache.bifromq.basekv.balance.util;
+import static com.google.protobuf.ByteString.copyFromUtf8;
import static org.apache.bifromq.basekv.balance.util.CommandUtil.diffBy;
import static org.apache.bifromq.basekv.utils.BoundaryUtil.toBoundary;
-import static com.google.protobuf.ByteString.copyFromUtf8;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
-import org.apache.bifromq.basekv.balance.BalanceNow;
-import org.apache.bifromq.basekv.balance.BalanceResult;
-import org.apache.bifromq.basekv.balance.BalanceResultType;
+import com.google.protobuf.ByteString;
+import java.util.Collections;
+import java.util.NavigableMap;
+import java.util.Set;
+import java.util.TreeMap;
import org.apache.bifromq.basekv.balance.command.BalanceCommand;
import org.apache.bifromq.basekv.balance.command.BootstrapCommand;
import org.apache.bifromq.basekv.balance.command.ChangeConfigCommand;
@@ -44,11 +46,6 @@ import org.apache.bifromq.basekv.utils.BoundaryUtil;
import org.apache.bifromq.basekv.utils.EffectiveRoute;
import org.apache.bifromq.basekv.utils.KVRangeIdUtil;
import org.apache.bifromq.basekv.utils.LeaderRange;
-import com.google.protobuf.ByteString;
-import java.util.Collections;
-import java.util.NavigableMap;
-import java.util.Set;
-import java.util.TreeMap;
import org.testng.annotations.Test;
public class CommandUtilTest {
@@ -65,10 +62,9 @@ public class CommandUtilTest {
.build())
.build();
- BalanceResult result = CommandUtil.quit(localStoreId, kvRangeDescriptor);
+ BalanceCommand result = CommandUtil.quit(localStoreId, kvRangeDescriptor);
- assertEquals(result.type(), BalanceResultType.BalanceNow);
- ChangeConfigCommand command = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
+ ChangeConfigCommand command = (ChangeConfigCommand) result;
assertEquals(command.getToStore(), localStoreId);
assertEquals(command.getKvRangeId(), kvRangeId);
@@ -89,10 +85,9 @@ public class CommandUtilTest {
.build())
.build();
- BalanceResult result = CommandUtil.quit(localStoreId, kvRangeDescriptor);
+ BalanceCommand result = CommandUtil.quit(localStoreId, kvRangeDescriptor);
- assertEquals(result.type(), BalanceResultType.BalanceNow);
- ChangeConfigCommand command = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
+ ChangeConfigCommand command = (ChangeConfigCommand) result;
assertEquals(command.getToStore(), localStoreId);
assertEquals(command.getKvRangeId(), kvRangeId);
@@ -112,10 +107,9 @@ public class CommandUtilTest {
.build())
.build();
- BalanceResult result = CommandUtil.quit(localStoreId, kvRangeDescriptor);
+ BalanceCommand result = CommandUtil.quit(localStoreId, kvRangeDescriptor);
- assertEquals(result.type(), BalanceResultType.BalanceNow);
- ChangeConfigCommand command = (ChangeConfigCommand) ((BalanceNow<?>) result).command;
+ ChangeConfigCommand command = (ChangeConfigCommand) result;
assertEquals(command.getToStore(), localStoreId);
assertEquals(command.getKvRangeId(), kvRangeId);
diff --git a/base-kv/base-kv-store-server/src/main/java/org/apache/bifromq/basekv/server/BaseKVStoreService.java b/base-kv/base-kv-store-server/src/main/java/org/apache/bifromq/basekv/server/BaseKVStoreService.java
index b3fb5711..15d96150 100644
--- a/base-kv/base-kv-store-server/src/main/java/org/apache/bifromq/basekv/server/BaseKVStoreService.java
+++ b/base-kv/base-kv-store-server/src/main/java/org/apache/bifromq/basekv/server/BaseKVStoreService.java
@@ -20,7 +20,6 @@
package org.apache.bifromq.basekv.server;
import static org.apache.bifromq.base.util.CompletableFutureUtil.unwrap;
-import static org.apache.bifromq.basekv.utils.BoundaryUtil.FULL_BOUNDARY;
import static org.apache.bifromq.baserpc.server.UnaryResponse.response;
import com.google.common.collect.Sets;
@@ -94,10 +93,11 @@ class BaseKVStoreService extends BaseKVStoreServiceGrpc.BaseKVStoreServiceImplBa
public void start() {
log.debug("Starting BaseKVStore service");
kvRangeStore.start(new AgentHostStoreMessenger(agentHost, clusterId, kvRangeStore.id()));
- kvRangeStore.bootstrap(KVRangeIdUtil.generate(), FULL_BOUNDARY);
landscapeReporter = metaService.landscapeReporter(clusterId, kvRangeStore.id());
// sync store descriptor via crdt
disposables.add(kvRangeStore.describe().subscribe(landscapeReporter::report));
+ disposables.add(landscapeReporter.refreshSignal()
+ .subscribe(ts -> landscapeReporter.report(kvRangeStore.describe().blockingFirst())));
log.debug("BaseKVStore service started");
}
diff --git a/base-kv/base-kv-store-server/src/main/java/org/apache/bifromq/basekv/store/range/KVRangeFSM.java b/base-kv/base-kv-store-server/src/main/java/org/apache/bifromq/basekv/store/range/KVRangeFSM.java
index af807d3f..0f60e10b 100644
--- a/base-kv/base-kv-store-server/src/main/java/org/apache/bifromq/basekv/store/range/KVRangeFSM.java
+++ b/base-kv/base-kv-store-server/src/main/java/org/apache/bifromq/basekv/store/range/KVRangeFSM.java
@@ -336,7 +336,7 @@ public class KVRangeFSM implements IKVRangeFSM {
statsCollector.tick();
dumpSessions.values().forEach(KVRangeDumpSession::tick);
shrinkWAL();
- checkZombieState();
+ checkAndRepairFromZombieState();
estimateSplitHint();
}
@@ -372,12 +372,10 @@ public class KVRangeFSM implements IKVRangeFSM {
.thenCompose(v -> statsCollector.stop())
.thenCompose(v -> mgmtTaskRunner.awaitDone())
.thenCompose(v -> wal.close())
- .thenCompose(v -> {
+ .thenCompose(v -> awaitShutdown(fsmExecutor))
+ .whenComplete((v, e) -> {
kvRange.close();
metricManager.close();
- return awaitShutdown(fsmExecutor);
- })
- .whenComplete((v, e) -> {
cmdFutures.values()
.forEach(f -> f.completeExceptionally(new KVRangeException.TryLater("Range closed")));
queryRunner.close();
@@ -646,29 +644,27 @@ public class KVRangeFSM implements IKVRangeFSM {
switch (entry.getTypeCase()) {
case CONFIG -> {
IKVRangeWriter<?> rangeWriter = kvRange.toWriter();
- applyConfigChange(entry.getTerm(), entry.getIndex(), entry.getConfig(), rangeWriter)
- .whenComplete((callback, e) -> {
- if (onDone.isCancelled()) {
- rangeWriter.abort();
- } else {
- try {
- if (e != null) {
- rangeWriter.abort();
- onDone.completeExceptionally(e);
- } else {
- rangeWriter.lastAppliedIndex(entry.getIndex());
- rangeWriter.done();
- callback.run();
- linearizer.afterLogApplied(entry.getIndex());
- metricManager.reportLastAppliedIndex(entry.getIndex());
- onDone.complete(null);
- }
- } catch (Throwable t) {
- log.error("Failed to apply log", t);
- onDone.completeExceptionally(t);
+ try {
+ Supplier<CompletableFuture<Void>> afterLogApplied = applyConfigChange(entry.getTerm(),
+ entry.getIndex(), entry.getConfig(), rangeWriter);
+ rangeWriter.lastAppliedIndex(entry.getIndex());
+ rangeWriter.done();
+ afterLogApplied.get()
+ .whenComplete((v, e) -> {
+ if (e != null) {
+ log.error("Failed to apply config change", e);
+ onDone.completeExceptionally(e);
+ } else {
+ linearizer.afterLogApplied(entry.getIndex());
+ metricManager.reportLastAppliedIndex(entry.getIndex());
+ onDone.complete(null);
}
- }
- });
+ });
+ } catch (Throwable t) {
+ rangeWriter.abort();
+ log.error("Failed to apply command", t);
+ onDone.completeExceptionally(t);
+ }
}
case DATA -> {
try {
@@ -723,18 +719,17 @@ public class KVRangeFSM implements IKVRangeFSM {
return onDone;
}
- private CompletableFuture<Runnable> applyConfigChange(long term, long index,
- ClusterConfig config,
- IKVRangeWritable<?> rangeWriter) {
- CompletableFuture<Runnable> onDone = new CompletableFuture<>();
+ private Supplier<CompletableFuture<Void>> applyConfigChange(long term,
+ long index,
+ ClusterConfig config,
+ IKVRangeWritable<?> rangeWriter) {
State state = rangeWriter.state();
log.info("Apply new config[term={}, index={}]: state={}, leader={}\n{}",
term, index, state, wal.isLeader(), config);
rangeWriter.clusterConfig(config);
if (config.getNextVotersCount() != 0 || config.getNextLearnersCount() != 0) {
// skip joint-config
- onDone.complete(NOOP);
- return onDone;
+ return () -> CompletableFuture.completedFuture(null);
}
Set<String> members = newHashSet();
members.addAll(config.getVotersList());
@@ -753,18 +748,17 @@ public class KVRangeFSM implements IKVRangeFSM {
.setType(Removed)
.setTaskId(taskId)
.build());
- onDone.complete(() -> {
+ return () -> {
quitSignal.complete(null);
finishCommand(taskId);
- });
+ return CompletableFuture.completedFuture(null);
+ };
} else {
rangeWriter.state(State.newBuilder()
.setType(Normal)
.setTaskId(taskId)
.build());
- onDone.complete(() -> {
- finishCommand(taskId);
- });
+ return () -> compactWAL().thenRun(() -> finishCommand(taskId));
}
} else {
// request config change failed, the config entry is appended due to leader reelection
@@ -774,16 +768,17 @@ public class KVRangeFSM implements IKVRangeFSM {
.setType(Removed)
.setTaskId(taskId)
.build());
- onDone.complete(() -> {
+ return () -> {
quitSignal.complete(null);
finishCommand(taskId);
- });
+ return CompletableFuture.completedFuture(null);
+ };
} else {
rangeWriter.state(State.newBuilder()
.setType(Normal)
.setTaskId(taskId)
.build());
- onDone.complete(() -> finishCommand(taskId));
+ return () -> compactWAL().thenRun(() -> finishCommand(taskId));
}
}
}
@@ -802,12 +797,13 @@ public class KVRangeFSM implements IKVRangeFSM {
.build());
}
rangeWriter.bumpVer(false);
- onDone.complete(() -> {
+ return () -> {
finishCommand(taskId);
if (remove) {
quitSignal.complete(null);
}
- });
+ return CompletableFuture.completedFuture(null);
+ };
}
case ToBePurged -> {
String taskId = state.getTaskId();
@@ -817,25 +813,28 @@ public class KVRangeFSM implements IKVRangeFSM {
.setType(Removed)
.setTaskId(taskId)
.build());
- onDone.complete(() -> {
+ return () -> {
finishCommand(taskId);
quitSignal.complete(null);
- });
+ return CompletableFuture.completedFuture(null);
+ };
} else {
rangeWriter.state(State.newBuilder()
.setType(Normal)
.setTaskId(taskId)
.build());
- onDone.complete(() -> {
+ return () -> compactWAL().thenRun(() -> {
+ // purge failed due to leader change, reset back to normal
+ log.debug("Purge failed due to leader change[newConfig={}]", config);
finishCommand(taskId);
});
}
}
- default ->
- // skip internal config change triggered by leadership change
- onDone.complete(NOOP);
+ default -> {
+ // skip internal config change triggered by leadership change, no need to compact WAL
+ return () -> CompletableFuture.completedFuture(null);
+ }
}
- return onDone;
}
private CompletableFuture<Runnable> applyCommand(long ver,
@@ -876,11 +875,6 @@ public class KVRangeFSM implements IKVRangeFSM {
// make a checkpoint if needed
CompletableFuture<Void> compactWALFuture = CompletableFuture.completedFuture(null);
if (wal.latestSnapshot().getLastAppliedIndex() < logIndex - 1) {
- // cancel all on-going dump sessions
- dumpSessions.forEach((sessionId, session) -> {
- session.cancel();
- dumpSessions.remove(sessionId, session);
- });
compactWALFuture = compactWAL();
}
compactWALFuture.whenCompleteAsync((v, e) -> {
@@ -905,67 +899,84 @@ public class KVRangeFSM implements IKVRangeFSM {
? newHashSet(clusterConfig.getVotersList()) : newHashSet(newConfig.getVotersList());
Set<String> nextLearners = toBePurged
? emptySet() : newHashSet(newConfig.getLearnersList());
- List<CompletableFuture<?>> onceFutures = newHostingStoreIds.stream()
- .map(storeId -> messenger
- .once(m -> {
- if (m.hasEnsureRangeReply()) {
- EnsureRangeReply reply = m.getEnsureRangeReply();
- return reply.getResult() == EnsureRangeReply.Result.OK;
- }
- return false;
- })
- .orTimeout(5, TimeUnit.SECONDS)
- )
- .collect(Collectors.toList());
- CompletableFuture.allOf(onceFutures.toArray(CompletableFuture[]::new))
- .whenCompleteAsync((v1, t) -> {
- if (t != null) {
- String errorMessage = String.format("ConfigChange aborted[taskId=%s] due to %s",
- taskId, t.getMessage());
- log.warn(errorMessage);
- finishCommandWithError(taskId, new KVRangeException.TryLater(errorMessage));
- wal.stepDown();
- return;
- }
- wal.changeClusterConfig(taskId, nextVoters, nextLearners)
- .whenCompleteAsync((v2, e2) -> {
- if (e2 != null) {
- String errorMessage =
- String.format("ConfigChange aborted[taskId=%s] due to %s",
- taskId, e2.getMessage());
- log.debug(errorMessage);
- finishCommandWithError(taskId, new KVRangeException.TryLater(errorMessage));
- wal.stepDown();
+ if (wal.isLeader()) {
+ List<CompletableFuture<?>> onceFutures = newHostingStoreIds.stream()
+ .map(storeId -> messenger
+ .once(m -> {
+ if (m.hasEnsureRangeReply()) {
+ EnsureRangeReply reply = m.getEnsureRangeReply();
+ return reply.getResult() == EnsureRangeReply.Result.OK;
}
- // postpone finishing command when config entry is applied
- }, fsmExecutor);
- }, fsmExecutor);
- newHostingStoreIds.forEach(storeId -> {
- log.debug("Send EnsureRequest: taskId={}, targetStoreId={}", taskId, storeId);
- ClusterConfig ensuredClusterConfig = ClusterConfig.getDefaultInstance();
- messenger.send(KVRangeMessage.newBuilder()
- .setRangeId(id)
- .setHostStoreId(storeId)
- .setEnsureRange(EnsureRange.newBuilder()
- .setVer(ver) // ensure the new kvrange is compatible in target store
- .setBoundary(boundary)
- .setInitSnapshot(Snapshot.newBuilder()
- .setTerm(0)
- .setIndex(0)
- .setClusterConfig(ensuredClusterConfig) // empty voter set
- .setData(KVRangeSnapshot.newBuilder()
- .setVer(ver)
- .setId(id)
- // no checkpoint specified
- .setLastAppliedIndex(0)
- .setBoundary(boundary)
- .setState(state)
- .setClusterConfig(ensuredClusterConfig)
- .build().toByteString())
+ return false;
+ })
+ .orTimeout(5, TimeUnit.SECONDS)
+ )
+ .collect(Collectors.toList());
+ CompletableFuture.allOf(onceFutures.toArray(CompletableFuture[]::new))
+ .whenCompleteAsync((v1, t) -> {
+ if (t != null) {
+ String errorMessage = String.format("ConfigChange aborted[taskId=%s] due to %s",
+ taskId, t.getMessage());
+ log.warn(errorMessage);
+ finishCommandWithError(taskId, new KVRangeException.TryLater(errorMessage));
+ wal.stepDown();
+ return;
+ }
+ wal.changeClusterConfig(taskId, nextVoters, nextLearners)
+ .whenCompleteAsync((v2, e2) -> {
+ if (e2 != null) {
+ String errorMessage =
+ String.format("ConfigChange aborted[taskId=%s] due to %s",
+ taskId, e2.getMessage());
+ log.debug(errorMessage);
+ finishCommandWithError(taskId,
+ new KVRangeException.TryLater(errorMessage));
+ wal.stepDown();
+ }
+ // postpone finishing command when config entry is applied
+ }, fsmExecutor);
+ }, fsmExecutor);
+ newHostingStoreIds.forEach(storeId -> {
+ log.debug("Send EnsureRequest: taskId={}, targetStoreId={}", taskId, storeId);
+ ClusterConfig ensuredClusterConfig = ClusterConfig.getDefaultInstance();
+ messenger.send(KVRangeMessage.newBuilder()
+ .setRangeId(id)
+ .setHostStoreId(storeId)
+ .setEnsureRange(EnsureRange.newBuilder()
+ .setVer(ver) // ensure the new kvrange is compatible in target store
+ .setBoundary(boundary)
+ .setInitSnapshot(Snapshot.newBuilder()
+ .setTerm(0)
+ .setIndex(0)
+ .setClusterConfig(ensuredClusterConfig) // empty voter set
+ .setData(KVRangeSnapshot.newBuilder()
+ .setVer(ver)
+ .setId(id)
+ // no checkpoint specified
+ .setLastAppliedIndex(0)
+ .setBoundary(boundary)
+ .setState(state)
+ .setClusterConfig(ensuredClusterConfig)
+ .build().toByteString())
+ .build())
.build())
- .build())
- .build());
- });
+ .build());
+ });
+ } else {
+ wal.changeClusterConfig(taskId, nextVoters, nextLearners)
+ .whenCompleteAsync((v2, e2) -> {
+ if (e2 != null) {
+ String errorMessage =
+ String.format("ConfigChange aborted[taskId=%s] due to %s",
+ taskId, e2.getMessage());
+ log.debug(errorMessage);
+ finishCommandWithError(taskId,
+ new KVRangeException.TryLater(errorMessage));
+ wal.stepDown();
+ }
+ // postpone finishing command when config entry is applied
+ }, fsmExecutor);
+ }
if (state.getType() == Normal) {
if (toBePurged) {
rangeWriter.state(State.newBuilder()
@@ -1495,7 +1506,6 @@ public class KVRangeFSM implements IKVRangeFSM {
return restorer.restoreFrom(leader, snapshot)
.handle((result, ex) -> {
if (ex != null) {
- log.warn("Restored from snapshot error: \n{}", snapshot, ex);
return onInstalled.call(null, ex);
} else {
return onInstalled.call(kvRange.checkpoint(), null);
@@ -1504,10 +1514,9 @@ public class KVRangeFSM implements IKVRangeFSM {
.thenCompose(f -> f)
.whenCompleteAsync(unwrap((v, e) -> {
if (e != null) {
- if (e instanceof SnapshotException) {
- log.error("Failed to apply snapshot to WAL \n{}", snapshot, e);
- // WAL and FSM are inconsistent, need to quit and recreate again
- quitSignal.complete(null);
+ if (e instanceof SnapshotException.ObsoleteSnapshotException) {
+ log.debug("Obsolete snapshot, reset kvRange to latest snapshot: \n{}", snapshot);
+ kvRange.toReseter(wal.latestSnapshot()).done();
}
} else {
linearizer.afterLogApplied(snapshot.getLastAppliedIndex());
@@ -1564,6 +1573,11 @@ public class KVRangeFSM implements IKVRangeFSM {
}
private CompletableFuture<Void> compactWAL() {
+ // cancel all on-going dump sessions
+ dumpSessions.forEach((sessionId, session) -> {
+ session.cancel();
+ dumpSessions.remove(sessionId, session);
+ });
return mgmtTaskRunner.add(this::doCompactWAL);
}
@@ -1592,7 +1606,7 @@ public class KVRangeFSM implements IKVRangeFSM {
}
}
- private void checkZombieState() {
+ private void checkAndRepairFromZombieState() {
if (zombieAt > 0
&& Duration.ofMillis(HLC.INST.getPhysical() - zombieAt).toSeconds() > opts.getZombieTimeoutSec()) {
ClusterConfig clusterConfig = wal.latestClusterConfig();
@@ -1604,7 +1618,8 @@ public class KVRangeFSM implements IKVRangeFSM {
clusterConfig);
wal.recover().whenComplete((v, e) -> recovering.set(false));
}
- } else {
+ } else if (!clusterConfig.getVotersList().contains(hostStoreId)
+ && !clusterConfig.getLearnersList().contains(hostStoreId)) {
log.info("Zombie state detected, send quit signal.");
quitSignal.complete(null);
}
diff --git a/base-kv/base-kv-store-server/src/main/java/org/apache/bifromq/basekv/store/wal/KVRangeWALSubscription.java b/base-kv/base-kv-store-server/src/main/java/org/apache/bifromq/basekv/store/wal/KVRangeWALSubscription.java
index 42f63ab7..c121a241 100644
--- a/base-kv/base-kv-store-server/src/main/java/org/apache/bifromq/basekv/store/wal/KVRangeWALSubscription.java
+++ b/base-kv/base-kv-store-server/src/main/java/org/apache/bifromq/basekv/store/wal/KVRangeWALSubscription.java
@@ -72,8 +72,7 @@ class KVRangeWALSubscription implements IKVRangeWALSubscription {
applyRunner.add(restore(task))
.handle((snap, e) -> fetchRunner.add(() -> {
if (e != null) {
- log.error(
- "Failed to install snapshot\n{}", snap);
+ log.error("Failed to restore from snapshot\n{}", task.snapshot, e);
return;
}
log.debug("Snapshot installed\n{}", snap);
diff --git a/base-kv/base-kv-type-proto/src/main/java/org/apache/bifromq/basekv/utils/DescriptorUtil.java b/base-kv/base-kv-type-proto/src/main/java/org/apache/bifromq/basekv/utils/DescriptorUtil.java
index c39045fa..44abd56e 100644
--- a/base-kv/base-kv-type-proto/src/main/java/org/apache/bifromq/basekv/utils/DescriptorUtil.java
+++ b/base-kv/base-kv-type-proto/src/main/java/org/apache/bifromq/basekv/utils/DescriptorUtil.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basekv.utils;
@@ -22,10 +22,6 @@ package org.apache.bifromq.basekv.utils;
import static org.apache.bifromq.basekv.utils.BoundaryUtil.endKey;
import static org.apache.bifromq.basekv.utils.BoundaryUtil.startKey;
-import org.apache.bifromq.basekv.proto.Boundary;
-import org.apache.bifromq.basekv.proto.KVRangeDescriptor;
-import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
-import org.apache.bifromq.basekv.raft.proto.RaftNodeStatus;
import com.google.protobuf.ByteString;
import java.util.Comparator;
import java.util.HashMap;
@@ -37,6 +33,10 @@ import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;
+import org.apache.bifromq.basekv.proto.Boundary;
+import org.apache.bifromq.basekv.proto.KVRangeDescriptor;
+import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
+import org.apache.bifromq.basekv.raft.proto.RaftNodeStatus;
/**
* Utilities for processing descriptor.
@@ -100,14 +100,21 @@ public class DescriptorUtil {
for (KVRangeStoreDescriptor storeDescriptor : effectiveEpoch.storeDescriptors()) {
for (KVRangeDescriptor rangeDescriptor : storeDescriptor.getRangesList()) {
if (rangeDescriptor.getRole() == RaftNodeStatus.Leader) {
- ByteString startKey = startKey(rangeDescriptor.getBoundary());
- if (startKey == null) {
- firstLeaderRanges.add(new LeaderRange(rangeDescriptor, storeDescriptor));
- continue;
+ switch (rangeDescriptor.getState()) {
+ case Normal, ConfigChanging, PreparedMerging, WaitingForMerge -> {
+ ByteString startKey = startKey(rangeDescriptor.getBoundary());
+ if (startKey == null) {
+ firstLeaderRanges.add(new LeaderRange(rangeDescriptor, storeDescriptor));
+ continue;
+ }
+ sortedLeaderRanges.computeIfAbsent(startKey,
+ k -> new TreeSet<>(Comparator.comparingLong(l -> l.descriptor().getId().getId())))
+ .add(new LeaderRange(rangeDescriptor, storeDescriptor));
+ }
+ default -> {
+ // skip other states
+ }
}
- sortedLeaderRanges.computeIfAbsent(startKey,
- k -> new TreeSet<>(Comparator.comparingLong(l -> l.descriptor().getId().getId())))
- .add(new LeaderRange(rangeDescriptor, storeDescriptor));
}
}
}
diff --git a/base-kv/base-kv-type-proto/src/test/java/org/apache/bifromq/basekv/utils/DescriptorUtilTest.java b/base-kv/base-kv-type-proto/src/test/java/org/apache/bifromq/basekv/utils/DescriptorUtilTest.java
index 3b724b5a..40b19a23 100644
--- a/base-kv/base-kv-type-proto/src/test/java/org/apache/bifromq/basekv/utils/DescriptorUtilTest.java
+++ b/base-kv/base-kv-type-proto/src/test/java/org/apache/bifromq/basekv/utils/DescriptorUtilTest.java
@@ -19,16 +19,15 @@
package org.apache.bifromq.basekv.utils;
+import static org.apache.bifromq.basekv.proto.State.StateType.Merged;
+import static org.apache.bifromq.basekv.proto.State.StateType.Normal;
+import static org.apache.bifromq.basekv.proto.State.StateType.PreparedMerging;
+import static org.apache.bifromq.basekv.proto.State.StateType.Removed;
import static org.apache.bifromq.basekv.utils.DescriptorUtil.getEffectiveEpoch;
import static org.apache.bifromq.basekv.utils.DescriptorUtil.organizeByEpoch;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
-import org.apache.bifromq.basekv.proto.Boundary;
-import org.apache.bifromq.basekv.proto.KVRangeDescriptor;
-import org.apache.bifromq.basekv.proto.KVRangeId;
-import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
-import org.apache.bifromq.basekv.raft.proto.RaftNodeStatus;
import com.google.protobuf.ByteString;
import java.util.Arrays;
import java.util.HashSet;
@@ -37,6 +36,11 @@ import java.util.Map;
import java.util.NavigableMap;
import java.util.Set;
import java.util.stream.Collectors;
+import org.apache.bifromq.basekv.proto.Boundary;
+import org.apache.bifromq.basekv.proto.KVRangeDescriptor;
+import org.apache.bifromq.basekv.proto.KVRangeId;
+import org.apache.bifromq.basekv.proto.KVRangeStoreDescriptor;
+import org.apache.bifromq.basekv.raft.proto.RaftNodeStatus;
import org.testng.annotations.Test;
public class DescriptorUtilTest {
@@ -460,6 +464,7 @@ public class DescriptorUtilTest {
KVRangeDescriptor r1 = KVRangeDescriptor.newBuilder()
.setId(id1)
.setRole(RaftNodeStatus.Leader)
+ .setState(Normal)
.setBoundary(boundaryBuilder1.build())
.build();
@@ -470,6 +475,7 @@ public class DescriptorUtilTest {
KVRangeDescriptor r2 = KVRangeDescriptor.newBuilder()
.setId(id2)
.setRole(RaftNodeStatus.Leader)
+ .setState(Normal)
.setBoundary(boundaryBuilder2.build())
.build();
@@ -479,6 +485,7 @@ public class DescriptorUtilTest {
KVRangeDescriptor r3 = KVRangeDescriptor.newBuilder()
.setId(id3)
.setRole(RaftNodeStatus.Leader)
+ .setState(Normal)
.setBoundary(boundaryBuilder3.build())
.build();
@@ -517,11 +524,13 @@ public class DescriptorUtilTest {
KVRangeDescriptor r1 = KVRangeDescriptor.newBuilder()
.setId(id1)
.setRole(RaftNodeStatus.Leader)
+ .setState(Normal)
.setBoundary(boundary)
.build();
KVRangeDescriptor r2 = KVRangeDescriptor.newBuilder()
.setId(id2)
.setRole(RaftNodeStatus.Leader)
+ .setState(Normal)
.setBoundary(boundary)
.build();
@@ -568,4 +577,257 @@ public class DescriptorUtilTest {
assertTrue(routeMap.isEmpty());
}
-}
\ No newline at end of file
+
+ @Test
+ public void getEffectiveRouteFiltersByState() {
+ KVRangeId id1 = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeId id2 = KVRangeId.newBuilder().setEpoch(1).setId(2).build();
+ KVRangeId id3 = KVRangeId.newBuilder().setEpoch(1).setId(3).build();
+ KVRangeId id4 = KVRangeId.newBuilder().setEpoch(1).setId(4).build();
+
+ // Allowed states
+ KVRangeDescriptor rNormal = KVRangeDescriptor.newBuilder()
+ .setId(id1)
+ .setRole(RaftNodeStatus.Leader)
+ .setBoundary(Boundary.newBuilder().setEndKey(ByteString.copyFromUtf8("b")).build())
+ .setState(Normal)
+ .build();
+ KVRangeDescriptor rPreparedMerging = KVRangeDescriptor.newBuilder()
+ .setId(id2)
+ .setRole(RaftNodeStatus.Leader)
+ .setBoundary(Boundary.newBuilder()
+ .setStartKey(ByteString.copyFromUtf8("b"))
+ .setEndKey(ByteString.copyFromUtf8("m"))
+ .build())
+ .setState(PreparedMerging)
+ .build();
+
+ // Disallowed states
+ KVRangeDescriptor rMerged = KVRangeDescriptor.newBuilder()
+ .setId(id3)
+ .setRole(RaftNodeStatus.Leader)
+ .setBoundary(Boundary.newBuilder()
+ .setStartKey(ByteString.copyFromUtf8("m"))
+ .setEndKey(ByteString.copyFromUtf8("z"))
+ .build())
+ .setState(Merged)
+ .build();
+ KVRangeDescriptor rRemoved = KVRangeDescriptor.newBuilder()
+ .setId(id4)
+ .setRole(RaftNodeStatus.Leader)
+ .setBoundary(Boundary.newBuilder()
+ .setStartKey(ByteString.copyFromUtf8("z"))
+ .build())
+ .setState(Removed)
+ .build();
+
+ KVRangeStoreDescriptor storeDescriptor = KVRangeStoreDescriptor.newBuilder()
+ .setId("store1")
+ .addRanges(rNormal)
+ .addRanges(rPreparedMerging)
+ .addRanges(rMerged)
+ .addRanges(rRemoved)
+ .build();
+
+ Set<KVRangeStoreDescriptor> storeDescriptors = new HashSet<>();
+ storeDescriptors.add(storeDescriptor);
+
+ EffectiveEpoch effectiveEpoch = new EffectiveEpoch(1, storeDescriptors);
+ EffectiveRoute effectiveRoute = DescriptorUtil.getEffectiveRoute(effectiveEpoch);
+ NavigableMap<Boundary, LeaderRange> routeMap = effectiveRoute.leaderRanges();
+
+ // Only two allowed ranges should be present
+ assertEquals(routeMap.size(), 2);
+ List<Long> ids = routeMap.values().stream().map(lr -> lr.descriptor().getId().getId()).toList();
+ assertTrue(ids.contains(1L));
+ assertTrue(ids.contains(2L));
+ }
+
+ @Test
+ public void getEffectiveRoutePrefersNullStartKeyAsFirst() {
+ KVRangeId id1 = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeId id2 = KVRangeId.newBuilder().setEpoch(1).setId(2).build();
+
+ // First range without startKey (should be chosen as first)
+ KVRangeDescriptor r1 = KVRangeDescriptor.newBuilder()
+ .setId(id1)
+ .setRole(RaftNodeStatus.Leader)
+ .setBoundary(Boundary.newBuilder()
+ .setEndKey(ByteString.copyFromUtf8("m"))
+ .build())
+ .setState(Normal)
+ .build();
+
+ // Second range with explicit startKey
+ KVRangeDescriptor r2 = KVRangeDescriptor.newBuilder()
+ .setId(id2)
+ .setRole(RaftNodeStatus.Leader)
+ .setBoundary(Boundary.newBuilder().setStartKey(ByteString.copyFromUtf8("m")).build())
+ .setState(Normal)
+ .build();
+
+ KVRangeStoreDescriptor storeDescriptor = KVRangeStoreDescriptor.newBuilder()
+ .setId("store1")
+ .addRanges(r2)
+ .addRanges(r1)
+ .build();
+
+ Set<KVRangeStoreDescriptor> storeDescriptors = new HashSet<>();
+ storeDescriptors.add(storeDescriptor);
+
+ EffectiveEpoch effectiveEpoch = new EffectiveEpoch(1, storeDescriptors);
+ NavigableMap<Boundary, LeaderRange> routeMap = DescriptorUtil.getEffectiveRoute(effectiveEpoch).leaderRanges();
+
+ assertEquals(routeMap.firstEntry().getValue().descriptor().getId(), id1);
+ }
+
+ @Test
+ public void getEffectiveRouteStopsAtNullEndKey() {
+ KVRangeId id1 = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeId id2 = KVRangeId.newBuilder().setEpoch(1).setId(2).build();
+ KVRangeId id3 = KVRangeId.newBuilder().setEpoch(1).setId(3).build();
+
+ KVRangeDescriptor r1 = KVRangeDescriptor.newBuilder()
+ .setId(id1)
+ .setRole(RaftNodeStatus.Leader)
+ .setBoundary(Boundary.newBuilder().setEndKey(ByteString.copyFromUtf8("b")).build())
+ .setState(Normal)
+ .build();
+ // Tail range with null endKey
+ KVRangeDescriptor r2 = KVRangeDescriptor.newBuilder()
+ .setId(id2)
+ .setRole(RaftNodeStatus.Leader)
+ .setBoundary(Boundary.newBuilder().setStartKey(ByteString.copyFromUtf8("b")).build())
+ .setState(Normal)
+ .build();
+ // An extra range that should never be reached after tail
+ KVRangeDescriptor r3 = KVRangeDescriptor.newBuilder()
+ .setId(id3)
+ .setRole(RaftNodeStatus.Leader)
+ .setBoundary(
+ Boundary.newBuilder().setStartKey(ByteString.copyFromUtf8("c")).setEndKey(ByteString.copyFromUtf8("d"))
+ .build())
+ .setState(Normal)
+ .build();
+
+ KVRangeStoreDescriptor storeDescriptor = KVRangeStoreDescriptor.newBuilder()
+ .setId("store1")
+ .addRanges(r1)
+ .addRanges(r2)
+ .addRanges(r3)
+ .build();
+
+ Set<KVRangeStoreDescriptor> storeDescriptors = new HashSet<>();
+ storeDescriptors.add(storeDescriptor);
+
+ EffectiveEpoch effectiveEpoch = new EffectiveEpoch(1, storeDescriptors);
+ NavigableMap<Boundary, LeaderRange> routeMap = DescriptorUtil.getEffectiveRoute(effectiveEpoch).leaderRanges();
+
+ // Should stop at r2 (endKey null)
+ assertEquals(routeMap.size(), 2);
+ List<Long> ids = routeMap.values().stream().map(lr -> lr.descriptor().getId().getId()).toList();
+ assertTrue(ids.contains(1L));
+ assertTrue(ids.contains(2L));
+ }
+
+ @Test
+ public void getEffectiveRouteAllowsGapsByCeilingStartKey() {
+ KVRangeId id1 = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeId id2 = KVRangeId.newBuilder().setEpoch(1).setId(2).build();
+
+ KVRangeDescriptor r1 = KVRangeDescriptor.newBuilder()
+ .setId(id1)
+ .setRole(RaftNodeStatus.Leader)
+ .setBoundary(Boundary.newBuilder()
+ .setEndKey(ByteString.copyFromUtf8("b"))
+ .build())
+ .setState(Normal)
+ .build();
+ KVRangeDescriptor r2 = KVRangeDescriptor.newBuilder()
+ .setId(id2)
+ .setRole(RaftNodeStatus.Leader)
+ .setBoundary(Boundary.newBuilder()
+ .setStartKey(ByteString.copyFromUtf8("c"))
+ .setEndKey(ByteString.copyFromUtf8("z"))
+ .build())
+ .setState(Normal)
+ .build();
+
+ KVRangeStoreDescriptor storeDescriptor = KVRangeStoreDescriptor.newBuilder()
+ .setId("store1")
+ .addRanges(r1)
+ .addRanges(r2)
+ .build();
+
+ Set<KVRangeStoreDescriptor> storeDescriptors = new HashSet<>();
+ storeDescriptors.add(storeDescriptor);
+
+ EffectiveEpoch effectiveEpoch = new EffectiveEpoch(1, storeDescriptors);
+ NavigableMap<Boundary, LeaderRange> routeMap = DescriptorUtil.getEffectiveRoute(effectiveEpoch).leaderRanges();
+ assertEquals(routeMap.size(), 2);
+ }
+
+ @Test
+ public void organizeByEpochRetainsStoresWithoutRangesInEpoch() {
+ // store1 has epoch 1 & 2 ranges, store2 has only epoch 2
+ KVRangeId id11 = KVRangeId.newBuilder().setEpoch(1).setId(1).build();
+ KVRangeId id21 = KVRangeId.newBuilder().setEpoch(2).setId(1).build();
+ KVRangeDescriptor r11 = KVRangeDescriptor.newBuilder()
+ .setId(id11)
+ .setBoundary(Boundary.newBuilder().setEndKey(ByteString.copyFromUtf8("m")).build())
+ .build();
+ KVRangeDescriptor r21 = KVRangeDescriptor.newBuilder()
+ .setId(id21)
+ .setBoundary(Boundary.newBuilder().setStartKey(ByteString.copyFromUtf8("n")).build())
+ .build();
+
+ KVRangeStoreDescriptor s1 = KVRangeStoreDescriptor.newBuilder().setId("store1").addRanges(r11).addRanges(r21)
+ .build();
+ KVRangeStoreDescriptor s2 = KVRangeStoreDescriptor.newBuilder().setId("store2").addRanges(r21).build();
+
+ Set<KVRangeStoreDescriptor> set = new HashSet<>();
+ set.add(s1);
+ set.add(s2);
+
+ NavigableMap<Long, Set<KVRangeStoreDescriptor>> byEpoch = DescriptorUtil.organizeByEpoch(set);
+ // Epoch 1 should contain both stores, with store2 having 0 ranges
+ assertTrue(byEpoch.containsKey(1L));
+ Set<KVRangeStoreDescriptor> epoch1 = byEpoch.get(1L);
+ assertEquals(epoch1.size(), 2);
+ for (KVRangeStoreDescriptor d : epoch1) {
+ if (d.getId().equals("store1")) {
+ assertEquals(d.getRangesCount(), 1);
+ assertEquals(d.getRanges(0).getId().getEpoch(), 1L);
+ } else if (d.getId().equals("store2")) {
+ assertEquals(d.getRangesCount(), 0);
+ }
+ }
+ }
+
+ @Test
+ public void getEffectiveEpochOldestSelectionWithMixedStores() {
+ // store1 has epoch 2, store2 has epoch 3; no epoch 1 present => pick epoch 2
+ KVRangeId id2 = KVRangeId.newBuilder().setEpoch(2).setId(1).build();
+ KVRangeId id3 = KVRangeId.newBuilder().setEpoch(3).setId(1).build();
+ KVRangeDescriptor r2 = KVRangeDescriptor.newBuilder().setId(id2).build();
+ KVRangeDescriptor r3 = KVRangeDescriptor.newBuilder().setId(id3).build();
+
+ KVRangeStoreDescriptor s1 = KVRangeStoreDescriptor.newBuilder().setId("s1").addRanges(r2).build();
+ KVRangeStoreDescriptor s2 = KVRangeStoreDescriptor.newBuilder().setId("s2").addRanges(r3).build();
+ Set<KVRangeStoreDescriptor> set = new HashSet<>();
+ set.add(s1);
+ set.add(s2);
+
+ Set<KVRangeStoreDescriptor> result = DescriptorUtil.getEffectiveEpoch(set).get().storeDescriptors();
+ assertEquals(result.size(), 2);
+ // All descriptors in effective epoch must be epoch 2 versions of both stores, with s2 having 0 ranges
+ for (KVRangeStoreDescriptor d : result) {
+ if (d.getId().equals("s1")) {
+ assertEquals(d.getRangesCount(), 1);
+ assertEquals(d.getRanges(0).getId().getEpoch(), 2L);
+ } else if (d.getId().equals("s2")) {
+ assertEquals(d.getRangesCount(), 0);
+ }
+ }
+ }
+}
diff --git a/base-logger/src/main/java/org/apache/bifromq/logger/MDCLogger.java b/base-logger/src/main/java/org/apache/bifromq/logger/MDCLogger.java
index 32c2dd5e..6d418ce5 100644
--- a/base-logger/src/main/java/org/apache/bifromq/logger/MDCLogger.java
+++ b/base-logger/src/main/java/org/apache/bifromq/logger/MDCLogger.java
@@ -74,12 +74,20 @@ public class MDCLogger implements org.slf4j.Logger {
if (lvl.isEmpty()) {
return;
}
+ Object[] evaluated = args;
+ if (args != null && args.length > 0) {
+ evaluated = new Object[args.length];
+ for (int i = 0; i < args.length; i++) {
+ Object a = args[i];
+ evaluated[i] = (a instanceof Supplier) ? ((Supplier<?>) a).get() : a;
+ }
+ }
for (int i = 0; i < tags.length; i += 2) {
MDC.put(tags[i], tags[i + 1]);
}
Map<String, String> extraCtx = extraContext();
extraCtx.forEach(MDC::put);
- delegate.log(marker, FQCN, lvl.get().toInt(), msg, args, t);
+ delegate.log(marker, FQCN, lvl.get().toInt(), msg, evaluated, t);
for (int i = 0; i < tags.length; i += 2) {
MDC.remove(tags[i]);
}
diff --git a/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/BiDiStream.java b/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/BiDiStream.java
index dcf7a135..46b0b2c5 100644
--- a/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/BiDiStream.java
+++ b/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/BiDiStream.java
@@ -21,8 +21,6 @@ package org.apache.bifromq.baserpc.client;
import static io.grpc.stub.ClientCalls.asyncBidiStreamingCall;
-import org.apache.bifromq.baserpc.RPCContext;
-import org.apache.bifromq.baserpc.client.util.FastBehaviorSubject;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.Context;
@@ -36,10 +34,11 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.baserpc.RPCContext;
+import org.apache.bifromq.baserpc.client.util.FastBehaviorSubject;
@Slf4j
class BiDiStream<InT, OutT> implements IBiDiStream<InT, OutT> {
- private final String tenantId;
private final String serverId;
private final ClientCallStreamObserver<InT> callStreamObserver;
private final Subject<OutT> outSubject = PublishSubject.create();
@@ -54,7 +53,6 @@ class BiDiStream<InT, OutT> implements IBiDiStream<InT, OutT> {
MethodDescriptor<InT, OutT> methodDescriptor,
Map<String, String> metadata,
CallOptions callOptions) {
- this.tenantId = tenantId;
this.serverId = serverId;
Context ctx = Context.ROOT.fork()
.withValue(RPCContext.TENANT_ID_CTX_KEY, tenantId)
diff --git a/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/ClientChannel.java b/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/ClientChannel.java
index f753ba63..e4dc3a4d 100644
--- a/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/ClientChannel.java
+++ b/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/ClientChannel.java
@@ -19,16 +19,6 @@
package org.apache.bifromq.baserpc.client;
-import org.apache.bifromq.baseenv.EnvProvider;
-import org.apache.bifromq.baseenv.NettyEnv;
-import org.apache.bifromq.baserpc.BluePrint;
-import org.apache.bifromq.baserpc.client.interceptor.TenantAwareClientInterceptor;
-import org.apache.bifromq.baserpc.client.loadbalancer.IServerSelector;
-import org.apache.bifromq.baserpc.client.loadbalancer.TrafficDirectiveLoadBalancerProvider;
-import org.apache.bifromq.baserpc.client.nameresolver.TrafficGovernorNameResolverProvider;
-import org.apache.bifromq.baserpc.client.util.FastBehaviorSubject;
-import org.apache.bifromq.baserpc.trafficgovernor.IRPCServiceLandscape;
-import org.apache.bifromq.baserpc.trafficgovernor.IRPCServiceTrafficService;
import com.google.common.util.concurrent.MoreExecutors;
import io.grpc.Channel;
import io.grpc.ConnectivityState;
@@ -50,6 +40,16 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.Builder;
+import org.apache.bifromq.baseenv.EnvProvider;
+import org.apache.bifromq.baseenv.NettyEnv;
+import org.apache.bifromq.baserpc.BluePrint;
+import org.apache.bifromq.baserpc.client.interceptor.TenantAwareClientInterceptor;
+import org.apache.bifromq.baserpc.client.loadbalancer.IServerSelector;
+import org.apache.bifromq.baserpc.client.loadbalancer.TrafficDirectiveLoadBalancerProvider;
+import org.apache.bifromq.baserpc.client.nameresolver.TrafficGovernorNameResolverProvider;
+import org.apache.bifromq.baserpc.client.util.FastBehaviorSubject;
+import org.apache.bifromq.baserpc.trafficgovernor.IRPCServiceLandscape;
+import org.apache.bifromq.baserpc.trafficgovernor.IRPCServiceTrafficService;
class ClientChannel implements IClientChannel {
private final String serviceUniqueName;
@@ -136,7 +136,7 @@ class ClientChannel implements IClientChannel {
@Override
public Observable<IServerSelector> serverSelectorObservable() {
- return serverSelectorSubject;
+ return serverSelectorSubject.distinctUntilChanged();
}
@Override
diff --git a/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/ManagedBiDiStream.java b/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/ManagedBiDiStream.java
index 27f63fcf..03159dcd 100644
--- a/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/ManagedBiDiStream.java
+++ b/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/ManagedBiDiStream.java
@@ -116,14 +116,14 @@ abstract class ManagedBiDiStream<InT, OutT> {
abstract void onServiceUnavailable();
private void reportNoServerAvailable() {
- log.debug("Stream@{} no server available to target: method={}",
- this.hashCode(), methodDescriptor.getBareMethodName());
+ log.debug("Stream@{} no server available to target: method={}, state={}",
+ this.hashCode(), methodDescriptor.getBareMethodName(), state.get());
onNoServerAvailable();
}
private void reportServiceUnavailable() {
- log.debug("Stream@{} service unavailable to target: method={}",
- this.hashCode(), methodDescriptor.getBareMethodName());
+ log.debug("Stream@{} service unavailable to target: method={}, state={}",
+ this.hashCode(), methodDescriptor.getBareMethodName(), state.get());
onServiceUnavailable();
}
@@ -246,12 +246,12 @@ abstract class ManagedBiDiStream<InT, OutT> {
private void gracefulRetarget() {
if (state.compareAndSet(State.Normal, State.PendingRetarget)) {
- log.debug("Stream@{} start graceful retarget process: method={}",
- this.hashCode(), methodDescriptor.getBareMethodName());
+ log.debug("Stream@{} start graceful retarget process: method={}, state={}",
+ this.hashCode(), methodDescriptor.getBareMethodName(), state.get());
if (prepareRetarget()) {
// if it's ready to retarget, close it and start a new one
- log.debug("Stream@{} close current bidi-stream immediately before retargeting: method={}",
- this.hashCode(), methodDescriptor.getBareMethodName());
+ log.debug("Stream@{} close current bidi-stream immediately before retargeting: method={}, state={}",
+ this.hashCode(), methodDescriptor.getBareMethodName(), state.get());
state.set(State.Retargeting);
bidiStream.get().close();
scheduleRetargetNow();
@@ -272,10 +272,12 @@ abstract class ManagedBiDiStream<InT, OutT> {
private void scheduleRetarget(Duration delay) {
if (retargetScheduled.compareAndSet(false, true)) {
- log.debug("Stream@{} schedule retarget task in {}ms: method={}",
- this.hashCode(), delay.toMillis(), methodDescriptor.getBareMethodName());
- CompletableFuture.runAsync(() -> retarget(this.serverSelector),
- CompletableFuture.delayedExecutor(delay.toMillis(), MILLISECONDS));
+ log.debug("Stream@{} schedule retarget task in {}ms: method={}, state={}",
+ this.hashCode(), delay.toMillis(), methodDescriptor.getBareMethodName(), state.get());
+ CompletableFuture.runAsync(() -> {
+ retargetScheduled.set(false);
+ retarget(this.serverSelector);
+ }, CompletableFuture.delayedExecutor(delay.toMillis(), MILLISECONDS));
}
}
@@ -328,7 +330,6 @@ abstract class ManagedBiDiStream<InT, OutT> {
}
}
}
- retargetScheduled.set(false);
if (serverSelector != this.serverSelector) {
// server selector has been changed, schedule a retarget
scheduleRetargetNow();
@@ -338,10 +339,11 @@ abstract class ManagedBiDiStream<InT, OutT> {
private void target(String serverId) {
if (state.compareAndSet(State.Init, State.Normal)
|| state.compareAndSet(State.StreamDisconnect, State.Normal)
+ || state.compareAndSet(State.PendingRetarget, State.Normal)
|| state.compareAndSet(State.NoServerAvailable, State.Normal)
|| state.compareAndSet(State.Retargeting, State.Normal)) {
- log.debug("Stream@{} build bidi-stream to target server[{}]: method={}",
- this.hashCode(), serverId, methodDescriptor.getBareMethodName());
+ log.debug("Stream@{} build stream to server[{}]: method={}, state={}",
+ this.hashCode(), serverId, methodDescriptor.getBareMethodName(), state.get());
BidiStreamContext<InT, OutT> bidiStreamContext = BidiStreamContext.from(new BiDiStream<>(
tenantId,
serverId,
@@ -350,13 +352,13 @@ abstract class ManagedBiDiStream<InT, OutT> {
metadataSupplier.get(),
callOptions));
bidiStream.set(bidiStreamContext);
- onStreamCreated();
bidiStreamContext.subscribe(this::onNext, this::onError, this::onCompleted);
bidiStreamContext.onReady(ts -> onStreamReady());
+ onStreamCreated();
}
if (bidiStream.get().bidiStream().isReady()) {
- log.debug("Stream@{} ready after build to server[{}]: method={}",
- this.hashCode(), serverId, methodDescriptor.getBareMethodName());
+ log.debug("Stream@{} ready: method={}, state={}",
+ this.hashCode(), methodDescriptor.getBareMethodName(), state.get());
onStreamReady();
}
}
@@ -367,8 +369,8 @@ abstract class ManagedBiDiStream<InT, OutT> {
if (state.get() == State.PendingRetarget && canStartRetarget()) {
// do not close the stream inline
CompletableFuture.runAsync(() -> {
- log.debug("Stream@{} close current bidi-stream before retargeting: method={}",
- this.hashCode(), methodDescriptor.getBareMethodName());
+ log.debug("Stream@{} close current stream before retargeting: method={}, state={}",
+ this.hashCode(), methodDescriptor.getBareMethodName(), state.get());
state.set(State.Retargeting);
bidiStream.get().close();
scheduleRetargetNow();
@@ -377,18 +379,33 @@ abstract class ManagedBiDiStream<InT, OutT> {
}
private void onError(Throwable t) {
- log.debug("BidiStream@{} error: method={}", this.hashCode(), methodDescriptor.getBareMethodName(), t);
- state.compareAndSet(State.Normal, State.StreamDisconnect);
+ log.debug("Stream@{} error: method={}, state={}",
+ this.hashCode(), methodDescriptor.getBareMethodName(), state.get(), t);
+ State s = state.get();
+ if (s == State.Normal || s == State.PendingRetarget) {
+ state.compareAndSet(s, State.StreamDisconnect);
+ }
onStreamError(t);
- scheduleRetargetWithRandomDelay();
+ if (s == State.PendingRetarget) {
+ scheduleRetargetNow();
+ } else {
+ scheduleRetargetWithRandomDelay();
+ }
}
private void onCompleted() {
- log.debug("BidiStream@{} complete: method={}", this.hashCode(), methodDescriptor.getBareMethodName());
+ log.debug("Stream@{} close by server: method={}, state={}",
+ this.hashCode(), methodDescriptor.getBareMethodName(), state.get());
// server gracefully close the stream
- state.compareAndSet(State.Normal, State.StreamDisconnect);
- onStreamError(new CancellationException("server close the bidi-stream"));
- scheduleRetargetWithRandomDelay();
+ State s = state.get();
+ if (s == State.Normal || s == State.PendingRetarget) {
+ state.compareAndSet(s, State.StreamDisconnect);
+ }
+ onStreamError(new CancellationException("Server shutdown"));
+ if (s == State.PendingRetarget) {
+ scheduleRetargetNow();
+ }
+ // wait for selector change to trigger retargeting
}
enum State {
diff --git a/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/loadbalancer/TrafficDirectiveLoadBalancer.java b/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/loadbalancer/TrafficDirectiveLoadBalancer.java
index 2539d9cd..1f6c3857 100644
--- a/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/loadbalancer/TrafficDirectiveLoadBalancer.java
+++ b/base-rpc/base-rpc-client/src/main/java/org/apache/bifromq/baserpc/client/loadbalancer/TrafficDirectiveLoadBalancer.java
@@ -19,17 +19,16 @@
package org.apache.bifromq.baserpc.client.loadbalancer;
-import static org.apache.bifromq.baserpc.client.loadbalancer.Constants.IN_PROC_SERVER_ATTR_KEY;
-import static org.apache.bifromq.baserpc.client.loadbalancer.Constants.SERVER_GROUP_TAG_ATTR_KEY;
-import static org.apache.bifromq.baserpc.client.loadbalancer.Constants.SERVER_ID_ATTR_KEY;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.ConnectivityState.CONNECTING;
import static io.grpc.ConnectivityState.IDLE;
import static io.grpc.ConnectivityState.READY;
import static io.grpc.ConnectivityState.SHUTDOWN;
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
+import static org.apache.bifromq.baserpc.client.loadbalancer.Constants.IN_PROC_SERVER_ATTR_KEY;
+import static org.apache.bifromq.baserpc.client.loadbalancer.Constants.SERVER_GROUP_TAG_ATTR_KEY;
+import static org.apache.bifromq.baserpc.client.loadbalancer.Constants.SERVER_ID_ATTR_KEY;
-import org.apache.bifromq.baseenv.EnvProvider;
import com.google.common.collect.Maps;
import io.grpc.Attributes;
import io.grpc.ConnectivityState;
@@ -48,6 +47,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.IntStream;
import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.baseenv.EnvProvider;
@Slf4j
public class TrafficDirectiveLoadBalancer extends LoadBalancer {
@@ -72,6 +72,12 @@ public class TrafficDirectiveLoadBalancer extends LoadBalancer {
this.currentPicker = new SubChannelPicker();
}
+ private static <T> Set<T> difference(Set<T> a, Set<T> b) {
+ Set<T> aCopy = new HashSet<>(a);
+ aCopy.removeAll(b);
+ return aCopy;
+ }
+
@Override
public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) {
log.debug("Handle traffic change: resolvedAddresses={}", resolvedAddresses);
@@ -183,25 +189,11 @@ public class TrafficDirectiveLoadBalancer extends LoadBalancer {
currentPicker.refresh(serverChannels);
helper.updateBalancingState(newState, currentPicker);
- Map<String, Boolean> allServers = currentServers;
- ITenantRouter tenantRouter =
- new TenantRouter(currentServers, currentTrafficDirective, currentServerGroupTags);
- updateListener.onUpdate(new IServerSelector() {
- @Override
- public boolean exists(String serverId) {
- return allServers.containsKey(serverId);
- }
-
- @Override
- public IServerGroupRouter get(String tenantId) {
- return tenantRouter.get(tenantId);
- }
-
- @Override
- public String toString() {
- return allServers.toString();
- }
- });
+ if (newState == READY || (newState == TRANSIENT_FAILURE && currentServers.isEmpty())) {
+ // notify when channel is ready or TRANSIENT_FAILURE state and no servers available
+ updateListener.onUpdate(
+ new TenantAwareServerSelector(currentServers, currentServerGroupTags, currentTrafficDirective));
+ }
}
balancingStateUpdateScheduled.set(false);
}
@@ -275,10 +267,4 @@ public class TrafficDirectiveLoadBalancer extends LoadBalancer {
subchannel.requestConnection();
}
}
-
- private static <T> Set<T> difference(Set<T> a, Set<T> b) {
- Set<T> aCopy = new HashSet<>(a);
- aCopy.removeAll(b);
- return aCopy;
- }
}
diff --git a/base-rpc/base-rpc-server/src/main/java/org/apache/bifromq/baserpc/server/AbstractResponsePipeline.java b/base-rpc/base-rpc-server/src/main/java/org/apache/bifromq/baserpc/server/AbstractResponsePipeline.java
index 2f740f0c..7439c173 100644
--- a/base-rpc/base-rpc-server/src/main/java/org/apache/bifromq/baserpc/server/AbstractResponsePipeline.java
+++ b/base-rpc/base-rpc-server/src/main/java/org/apache/bifromq/baserpc/server/AbstractResponsePipeline.java
@@ -14,13 +14,11 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.baserpc.server;
-import org.apache.bifromq.base.util.FutureTracker;
-import org.apache.bifromq.baserpc.metrics.RPCMetric;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import io.micrometer.core.instrument.Timer;
@@ -28,6 +26,8 @@ import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.base.util.FutureTracker;
+import org.apache.bifromq.baserpc.metrics.RPCMetric;
@Slf4j
abstract class AbstractResponsePipeline<RequestT, ResponseT> extends AbstractStreamObserver<RequestT, ResponseT> {
@@ -40,6 +40,7 @@ abstract class AbstractResponsePipeline<RequestT, ResponseT> extends AbstractStr
protected AbstractResponsePipeline(StreamObserver<ResponseT> responseObserver) {
super(responseObserver);
+ this.responseObserver.setOnCancelHandler(this::cleanup);
}
@Override
@@ -80,7 +81,7 @@ abstract class AbstractResponsePipeline<RequestT, ResponseT> extends AbstractStr
/**
* Handle the request and return the result via completable future, remember always throw exception asynchronously
- * Returned future complete exceptionally will cause pipeline close
+ * Returned future complete exceptionally will cause pipeline close.
*
* @param tenantId the tenantId
* @param request the request
@@ -110,7 +111,6 @@ abstract class AbstractResponsePipeline<RequestT, ResponseT> extends AbstractStr
return respFuture;
}
-
final void emitResponse(RequestT req, ResponseT resp) {
if (!isClosed()) {
log.trace("Response sent in pipeline@{}: request={}, response={}", hashCode(), req, resp);
@@ -125,7 +125,6 @@ abstract class AbstractResponsePipeline<RequestT, ResponseT> extends AbstractStr
protected void afterClose() {
}
-
private void fail(Throwable throwable) {
if (!isClosed()) {
if (throwable instanceof CancellationException) {
diff --git a/base-rpc/base-rpc-traffic-governor/pom.xml b/base-rpc/base-rpc-traffic-governor/pom.xml
index 7e3e47a0..78932bdb 100644
--- a/base-rpc/base-rpc-traffic-governor/pom.xml
+++ b/base-rpc/base-rpc-traffic-governor/pom.xml
@@ -38,10 +38,26 @@
<groupId>org.apache.bifromq</groupId>
<artifactId>base-rpc-grpc-inproc</artifactId>
</dependency>
+ <!-- test dependencies -->
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-slf4j2-impl</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/base-rpc/base-rpc-traffic-governor/src/main/java/org/apache/bifromq/baserpc/trafficgovernor/RPCServiceTrafficManager.java b/base-rpc/base-rpc-traffic-governor/src/main/java/org/apache/bifromq/baserpc/trafficgovernor/RPCServiceTrafficManager.java
index 4cd67863..b0724d0c 100644
--- a/base-rpc/base-rpc-traffic-governor/src/main/java/org/apache/bifromq/baserpc/trafficgovernor/RPCServiceTrafficManager.java
+++ b/base-rpc/base-rpc-traffic-governor/src/main/java/org/apache/bifromq/baserpc/trafficgovernor/RPCServiceTrafficManager.java
@@ -14,24 +14,20 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.baserpc.trafficgovernor;
-import static org.apache.bifromq.baserpc.trafficgovernor.SharedScheduler.RPC_SHARED_SCHEDULER;
import static java.util.Collections.emptySet;
+import static org.apache.bifromq.baserpc.trafficgovernor.SharedScheduler.RPC_SHARED_SCHEDULER;
-import org.apache.bifromq.basecrdt.service.ICRDTService;
-import org.apache.bifromq.basehlc.HLC;
-import org.apache.bifromq.baserpc.proto.RPCServer;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.google.protobuf.ByteString;
import io.grpc.inprocess.InProcessSocketAddress;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
-import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
import java.net.InetSocketAddress;
import java.util.Map;
@@ -41,6 +37,10 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.base.util.RendezvousHash;
+import org.apache.bifromq.basecrdt.service.ICRDTService;
+import org.apache.bifromq.basehlc.HLC;
+import org.apache.bifromq.baserpc.proto.RPCServer;
@Slf4j
class RPCServiceTrafficManager extends RPCServiceAnnouncer
@@ -131,8 +131,8 @@ class RPCServiceTrafficManager extends RPCServiceAnnouncer
for (RPCServer server : announcedServers.values()) {
if (aliveAnnouncers.contains(server.getAnnouncerId())) {
aliveServers.add(build(server));
- } else {
- // this is a side effect: revoke the announcement made by dead announcer
+ } else if (shouldClean(aliveAnnouncers, server.getAnnouncerId())) {
+ // revoke the announcement made by dead announcer
log.debug("Remove not alive server announcement: {}", server.getId());
revoke(server.getId());
}
@@ -140,6 +140,18 @@ class RPCServiceTrafficManager extends RPCServiceAnnouncer
return aliveServers;
}
+ private boolean shouldClean(Set<ByteString> aliveAnnouncers, ByteString failedAnnouncer) {
+ aliveAnnouncers.add(id());
+ RendezvousHash<ByteString, ByteString> hash = RendezvousHash.<ByteString, ByteString>builder()
+ .keyFunnel((from, into) -> into.putBytes(from.asReadOnlyByteBuffer()))
+ .nodeFunnel((from, into) -> into.putBytes(from.asReadOnlyByteBuffer()))
+ .nodes(aliveAnnouncers)
+ .build();
+ ByteString cleaner = hash.get(failedAnnouncer);
+ return cleaner.equals(id());
+ }
+
+
private ServerEndpoint build(RPCServer server) {
return new ServerEndpoint(server.getAgentHostId(),
server.getId(),
@@ -157,41 +169,49 @@ class RPCServiceTrafficManager extends RPCServiceAnnouncer
private final RPCServiceTrafficManager manager;
private final AtomicReference<RPCServer> localServer;
- private final Disposable disposable;
- private final CompositeDisposable disposables;
+ private final CompositeDisposable myDisposibles = new CompositeDisposable();
+ private final CompositeDisposable allDisposibles;
- private ServerRegistration(RPCServer server, RPCServiceTrafficManager announcer,
- CompositeDisposable disposables) {
+ private ServerRegistration(RPCServer server,
+ RPCServiceTrafficManager announcer,
+ CompositeDisposable allDisposables) {
this.localServer = new AtomicReference<>(server);
this.manager = announcer;
- this.disposables = disposables;
+ this.allDisposibles = allDisposables;
// make an announcement via rpcServiceCRDT
log.debug("Announce local server[{}]:{}", announcer.serviceUniqueName, server);
announcer.announce(localServer.get()).join();
// enforce the announcement consistent eventually
- disposable = announcer.announcedServers()
+ myDisposibles.add(announcer.announcedServers()
.doOnDispose(() -> manager.revoke(localServer.get().getId()).join())
.subscribe(serverMap -> {
RPCServer localServer = this.localServer.get();
if (!serverMap.containsKey(localServer.getId())) {
- RPCServer toUpdate = localServer.toBuilder().setAnnouncedTS(HLC.INST.get()).build();
- log.debug("Re-announce local server: {}", toUpdate);
- // refresh announcement time
- announcer.announce(toUpdate);
+ reannounce();
} else if (localServer.getAnnouncedTS() < serverMap.get(localServer.getId()).getAnnouncedTS()) {
localServer = serverMap.get(localServer.getId());
log.debug("Update local server from announcement: server={}", localServer);
}
- });
- disposables.add(disposable);
+ }));
+ myDisposibles.add(announcer.crdtService.refreshSignal()
+ .subscribe(ts -> reannounce()));
+ allDisposables.add(myDisposibles);
+ }
+
+ private void reannounce() {
+ RPCServer localServer = this.localServer.get();
+ RPCServer toUpdate = localServer.toBuilder().setAnnouncedTS(HLC.INST.get()).build();
+ log.debug("Re-announce local server: {}", toUpdate);
+ // refresh announcement time
+ manager.announce(toUpdate);
}
@Override
public void stop() {
- disposables.remove(disposable);
- disposable.dispose();
+ allDisposibles.remove(myDisposibles);
+ myDisposibles.dispose();
}
}
}
diff --git a/base-scheduler/src/main/java/org/apache/bifromq/basescheduler/Batcher.java b/base-scheduler/src/main/java/org/apache/bifromq/basescheduler/Batcher.java
index d443430a..754f4628 100644
--- a/base-scheduler/src/main/java/org/apache/bifromq/basescheduler/Batcher.java
+++ b/base-scheduler/src/main/java/org/apache/bifromq/basescheduler/Batcher.java
@@ -14,32 +14,33 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basescheduler;
-import org.apache.bifromq.basescheduler.exception.BackPressureException;
-import org.apache.bifromq.basescheduler.spi.ICapacityEstimator;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
-import java.util.ArrayDeque;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.basescheduler.exception.BackPressureException;
+import org.apache.bifromq.basescheduler.spi.ICapacityEstimator;
@Slf4j
final class Batcher<CallT, CallResultT, BatcherKeyT> {
@@ -72,7 +73,7 @@ final class Batcher<CallT, CallResultT, BatcherKeyT> {
this.batchCallBuilder = batchCallBuilder;
this.capacityEstimator = capacityEstimator;
this.maxBurstLatency = maxBurstLatency;
- this.batchPool = new ArrayDeque<>();
+ this.batchPool = new ConcurrentLinkedDeque<>();
this.emaQueueingTime = new EMALong(System::nanoTime, 0.1, 0.9, maxBurstLatency);
Tags tags = Tags.of("name", name, "key", Integer.toUnsignedString(System.identityHashCode(this)));
maxPipelineDepthGauge = Gauge.builder("batcher.pipeline.max", capacityEstimator::maxPipelineDepth)
@@ -96,7 +97,7 @@ final class Batcher<CallT, CallResultT, BatcherKeyT> {
return CompletableFuture.failedFuture(
new RejectedExecutionException("Batcher has been shut down"));
}
- if (emaQueueingTime.get() < maxBurstLatency) {
+ if (Math.max(emaQueueingTime.get(), headCallWaitingNanos()) < maxBurstLatency) {
ICallTask<CallT, CallResultT, BatcherKeyT> callTask = new CallTask<>(batcherKey, request);
boolean offered = callTaskBuffers.offer(callTask);
assert offered;
@@ -104,10 +105,18 @@ final class Batcher<CallT, CallResultT, BatcherKeyT> {
return callTask.resultPromise();
} else {
dropCounter.increment();
- return CompletableFuture.failedFuture(new BackPressureException("Too high average latency"));
+ return CompletableFuture.failedFuture(new BackPressureException("Batch call busy"));
}
}
+ private long headCallWaitingNanos() {
+ ICallTask<CallT, CallResultT, BatcherKeyT> head = callTaskBuffers.peek();
+ if (head != null) {
+ return System.nanoTime() - head.ts();
+ }
+ return 0;
+ }
+
public CompletableFuture<Void> close() {
if (state.compareAndSet(State.RUNNING, State.SHUTTING_DOWN)) {
checkShutdownCompletion();
@@ -137,6 +146,7 @@ final class Batcher<CallT, CallResultT, BatcherKeyT> {
while ((batchCall = batchPool.poll()) != null) {
batchCall.destroy();
}
+ batchCallBuilder.close();
}
private void trigger() {
@@ -178,30 +188,37 @@ final class Batcher<CallT, CallResultT, BatcherKeyT> {
int finalBatchSize = batchSize;
CompletableFuture<Void> future = batchCall.execute();
runningBatchCalls.add(future);
- future.whenComplete((v, e) -> {
- runningBatchCalls.remove(future);
- long execEnd = System.nanoTime();
- if (e != null) {
- batchedTasks.forEach(t -> t.resultPromise().completeExceptionally(e));
- } else {
- long batchCallLatency = execEnd - execBegin;
- capacityEstimator.record(finalBatchSize, batchCallLatency);
- batchExecTimer.record(batchCallLatency, TimeUnit.NANOSECONDS);
- batchedTasks.forEach(t -> {
- long callLatency = execEnd - t.ts();
- batchCallTimer.record(callLatency, TimeUnit.NANOSECONDS);
- });
- }
- returnBatchCall(batchCall);
- pipelineDepth.getAndDecrement();
- // After each completion, check for shutdown
- if (state.get() == State.SHUTTING_DOWN) {
- checkShutdownCompletion();
- }
- if (!callTaskBuffers.isEmpty()) {
- trigger();
- }
- });
+ future
+ .orTimeout(maxBurstLatency, TimeUnit.NANOSECONDS) // Ensure we don't block indefinitely
+ .whenComplete((v, e) -> {
+ runningBatchCalls.remove(future);
+ long execEnd = System.nanoTime();
+ if (e != null) {
+ if (e instanceof TimeoutException) {
+ batchedTasks.forEach(t -> t.resultPromise()
+ .completeExceptionally(new BackPressureException("Batch Call timeout", e)));
+ } else {
+ batchedTasks.forEach(t -> t.resultPromise().completeExceptionally(e));
+ }
+ } else {
+ long batchCallLatency = execEnd - execBegin;
+ capacityEstimator.record(finalBatchSize, batchCallLatency);
+ batchExecTimer.record(batchCallLatency, TimeUnit.NANOSECONDS);
+ batchedTasks.forEach(t -> {
+ long callLatency = execEnd - t.ts();
+ batchCallTimer.record(callLatency, TimeUnit.NANOSECONDS);
+ });
+ }
+ returnBatchCall(batchCall);
+ pipelineDepth.getAndDecrement();
+ // After each completion, check for shutdown
+ if (state.get() == State.SHUTTING_DOWN) {
+ checkShutdownCompletion();
+ }
+ if (!callTaskBuffers.isEmpty()) {
+ trigger();
+ }
+ });
} catch (Throwable e) {
log.error("Batch call failed unexpectedly", e);
batchedTasks.forEach(t -> t.resultPromise().completeExceptionally(e));
diff --git a/base-scheduler/src/main/java/org/apache/bifromq/basescheduler/EMALong.java b/base-scheduler/src/main/java/org/apache/bifromq/basescheduler/EMALong.java
index 0f8d898c..f412a559 100644
--- a/base-scheduler/src/main/java/org/apache/bifromq/basescheduler/EMALong.java
+++ b/base-scheduler/src/main/java/org/apache/bifromq/basescheduler/EMALong.java
@@ -19,43 +19,58 @@
package org.apache.bifromq.basescheduler;
-import java.util.concurrent.atomic.AtomicLong;
+import com.google.common.base.Preconditions;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
class EMALong {
private static final double NANOS_PER_SECOND = 1_000_000_000.0;
private final Supplier<Long> nowSupplier;
- private final double alpha;
- private final double decay;
+ private final double alpha; // (0,1]
+ private final double decay; // (0,1]
private final long decayDelayNanos;
- private final AtomicLong value = new AtomicLong(0);
- private final AtomicLong lastUpdateTime = new AtomicLong(0);
+ private final AtomicReference<State> state;
public EMALong(Supplier<Long> nowSupplier, double alpha, double decay, long decayDelayNanos) {
+ Preconditions.checkArgument(alpha > 0.0 && alpha <= 1.0, "alpha must be in (0,1]");
+ Preconditions.checkArgument(decay > 0.0 && decay <= 1.0, "decay must be in (0,1]");
+ Preconditions.checkArgument(decayDelayNanos >= 0, "decayDelayNanos must be non-negative");
this.nowSupplier = nowSupplier;
this.alpha = alpha;
this.decay = decay;
this.decayDelayNanos = decayDelayNanos;
+ this.state = new AtomicReference<>(new State(0L, 0L));
}
public void update(long newValue) {
- value.updateAndGet(v -> {
- lastUpdateTime.set(nowSupplier.get());
- if (v == 0) {
- return newValue;
- } else {
- return (long) Math.ceil(v * (1 - alpha) + newValue * alpha);
+ long now = nowSupplier.get();
+ while (true) {
+ State prev = state.get();
+ long newEma = (prev.ema == 0L) ? newValue : (long) Math.ceil(prev.ema * (1 - alpha) + newValue * alpha);
+ State next = new State(newEma, now);
+ if (state.compareAndSet(prev, next)) {
+ return;
}
- });
+ }
}
public long get() {
long now = nowSupplier.get();
- long lastUpdate = lastUpdateTime.get();
- if (decayDelayNanos < Long.MAX_VALUE && lastUpdate + decayDelayNanos < now) {
- return (long) (value.get()
- * Math.pow(decay, Math.ceil((now - lastUpdate - decayDelayNanos) / NANOS_PER_SECOND)));
+ State s = state.get();
+ if (s.ema == 0L || s.lastTs == 0L) {
+ return s.ema;
+ }
+ if (decayDelayNanos < Long.MAX_VALUE) {
+ long dt = now - s.lastTs;
+ if (dt > decayDelayNanos) {
+ double seconds = Math.ceil((dt - decayDelayNanos) / NANOS_PER_SECOND);
+ double decayed = s.ema * Math.pow(decay, seconds);
+ return decayed < 1.0 ? 0L : Math.round(decayed);
+ }
}
- return value.get();
+ return s.ema;
+ }
+
+ private record State(long ema, long lastTs) {
}
}
diff --git a/base-scheduler/src/test/java/org/apache/bifromq/basescheduler/BatchCallSchedulerTest.java b/base-scheduler/src/test/java/org/apache/bifromq/basescheduler/BatchCallSchedulerTest.java
index 21d3b63e..1661d540 100644
--- a/base-scheduler/src/test/java/org/apache/bifromq/basescheduler/BatchCallSchedulerTest.java
+++ b/base-scheduler/src/test/java/org/apache/bifromq/basescheduler/BatchCallSchedulerTest.java
@@ -14,14 +14,14 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basescheduler;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
-import org.apache.bifromq.basescheduler.exception.BackPressureException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@@ -33,6 +33,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.basescheduler.exception.BackPressureException;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -61,9 +62,7 @@ public class BatchCallSchedulerTest {
executor.submit(() -> {
int i;
while ((i = count.decrementAndGet()) >= 0) {
- scheduler.schedule(i).whenComplete((v, e) -> {
- latch.countDown();
- });
+ scheduler.schedule(i).whenComplete((v, e) -> latch.countDown());
}
});
latch.await();
@@ -93,4 +92,17 @@ public class BatchCallSchedulerTest {
assertEquals(e.getCause().getClass(), BackPressureException.class);
}
}
+
+ @Test
+ public void batchCallTimeout() {
+ TestBatchCallScheduler scheduler =
+ new TestBatchCallScheduler(1, Duration.ofNanos(Long.MAX_VALUE), Duration.ofSeconds(1));
+ try {
+ scheduler.schedule(1).join();
+ fail();
+ } catch (Throwable e) {
+ assertEquals(e.getCause().getClass(), BackPressureException.class);
+ }
+ scheduler.close();
+ }
}
diff --git a/base-scheduler/src/test/java/org/apache/bifromq/basescheduler/EMALongTest.java b/base-scheduler/src/test/java/org/apache/bifromq/basescheduler/EMALongTest.java
index 49fd1726..3ca03f62 100644
--- a/base-scheduler/src/test/java/org/apache/bifromq/basescheduler/EMALongTest.java
+++ b/base-scheduler/src/test/java/org/apache/bifromq/basescheduler/EMALongTest.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.basescheduler;
@@ -81,10 +81,10 @@ public class EMALongTest {
void testDecayAfterDelay() {
// set decay=0.5, decayDelay=1s
EMALong ema = new EMALong(nowSupplier, 0.5, 0.5, 1_000_000_000L);
- fakeTime.set(0L);
+ fakeTime.set(1L);
ema.update(100L);
// advance time to after delay + 2s total => one decay period
- fakeTime.set(1_000_000_000L + 1_000_000_000L);
+ fakeTime.set(1_000_000_001L + 1_000_000_000L);
// (now - lastUpdate - delay) / 1e9 = (2s - 1s)/1e9 = 1 => ceil(1) =1
// value * decay^1 = 100 * 0.5 = 50
assertEquals(ema.get(), 50);
@@ -94,7 +94,7 @@ public class EMALongTest {
void testMultipleDecayPeriods() {
// decay=0.5, delay=1s
EMALong ema = new EMALong(nowSupplier, 0.5, 0.5, 1_000_000_000L);
- fakeTime.set(0L);
+ fakeTime.set(1L);
ema.update(80L);
// advance time to after delay + 3.2s => ceil(3.2)=4 periods
fakeTime.set(1_000_000_000L + 3_200_000_000L);
diff --git a/base-util/pom.xml b/base-util/pom.xml
index ca88f6c8..701acd89 100644
--- a/base-util/pom.xml
+++ b/base-util/pom.xml
@@ -31,6 +31,10 @@
<artifactId>base-util</artifactId>
<dependencies>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
diff --git a/bifromq-dist/bifromq-dist-worker/src/main/java/org/apache/bifromq/dist/worker/RendezvousHash.java b/base-util/src/main/java/org/apache/bifromq/base/util/RendezvousHash.java
similarity index 94%
rename from bifromq-dist/bifromq-dist-worker/src/main/java/org/apache/bifromq/dist/worker/RendezvousHash.java
rename to base-util/src/main/java/org/apache/bifromq/base/util/RendezvousHash.java
index b5ec1219..583e3348 100644
--- a/bifromq-dist/bifromq-dist-worker/src/main/java/org/apache/bifromq/dist/worker/RendezvousHash.java
+++ b/base-util/src/main/java/org/apache/bifromq/base/util/RendezvousHash.java
@@ -14,10 +14,10 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
-package org.apache.bifromq.dist.worker;
+package org.apache.bifromq.base.util;
import static com.google.common.hash.Hashing.murmur3_128;
@@ -31,7 +31,7 @@ import lombok.Builder;
* @param <N> The type of the node.
*/
@Builder
-class RendezvousHash<K, N> {
+public class RendezvousHash<K, N> {
private final Funnel<K> keyFunnel;
private final Funnel<N> nodeFunnel;
private final Iterable<N> nodes;
diff --git a/bifromq-dist/bifromq-dist-worker/src/test/java/org/apache/bifromq/dist/worker/RendezvousHashTest.java b/base-util/src/test/java/org/apache/bifromq/base/util/RendezvousHashTest.java
similarity index 98%
rename from bifromq-dist/bifromq-dist-worker/src/test/java/org/apache/bifromq/dist/worker/RendezvousHashTest.java
rename to base-util/src/test/java/org/apache/bifromq/base/util/RendezvousHashTest.java
index a0873337..55b954c3 100644
--- a/bifromq-dist/bifromq-dist-worker/src/test/java/org/apache/bifromq/dist/worker/RendezvousHashTest.java
+++ b/base-util/src/test/java/org/apache/bifromq/base/util/RendezvousHashTest.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.apache.bifromq.dist.worker;
+package org.apache.bifromq.base.util;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
diff --git a/bifromq-dist/bifromq-dist-server/src/test/java/org/apache/bifromq/dist/server/DistServiceTest.java b/bifromq-dist/bifromq-dist-server/src/test/java/org/apache/bifromq/dist/server/DistServiceTest.java
index edb96f62..b65f248f 100644
--- a/bifromq-dist/bifromq-dist-server/src/test/java/org/apache/bifromq/dist/server/DistServiceTest.java
+++ b/bifromq-dist/bifromq-dist-server/src/test/java/org/apache/bifromq/dist/server/DistServiceTest.java
@@ -135,6 +135,7 @@ public abstract class DistServiceTest {
.storeOptions(kvRangeStoreOptions)
.subBrokerManager(subBrokerMgr)
.settingProvider(settingProvider)
+ .bootstrapDelay(Duration.ofSeconds(1))
.build();
distServer = IDistServer.builder()
.rpcServerBuilder(rpcServerBuilder)
@@ -145,7 +146,7 @@ public abstract class DistServiceTest {
rpcServer = rpcServerBuilder.build();
rpcServer.start();
- await().until(() -> BoundaryUtil.isValidSplitSet(workerClient.latestEffectiveRouter().keySet()));
+ await().forever().until(() -> BoundaryUtil.isValidSplitSet(workerClient.latestEffectiveRouter().keySet()));
distClient.connState().filter(s -> s == IRPCClient.ConnState.READY).blockingFirst();
log.info("Setup finished, and start testing");
}
diff --git a/bifromq-dist/bifromq-dist-worker/src/main/java/org/apache/bifromq/dist/worker/DeliverExecutorGroup.java b/bifromq-dist/bifromq-dist-worker/src/main/java/org/apache/bifromq/dist/worker/DeliverExecutorGroup.java
index e0f87f39..70fcdb47 100644
--- a/bifromq-dist/bifromq-dist-worker/src/main/java/org/apache/bifromq/dist/worker/DeliverExecutorGroup.java
+++ b/bifromq-dist/bifromq-dist-worker/src/main/java/org/apache/bifromq/dist/worker/DeliverExecutorGroup.java
@@ -39,6 +39,7 @@ import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
+import org.apache.bifromq.base.util.RendezvousHash;
import org.apache.bifromq.deliverer.IMessageDeliverer;
import org.apache.bifromq.deliverer.TopicMessagePackHolder;
import org.apache.bifromq.dist.worker.schema.GroupMatching;
diff --git a/bifromq-dist/bifromq-dist-worker/src/test/java/org/apache/bifromq/dist/worker/DistWorkerTest.java b/bifromq-dist/bifromq-dist-worker/src/test/java/org/apache/bifromq/dist/worker/DistWorkerTest.java
index 106d2900..975f0e1d 100644
--- a/bifromq-dist/bifromq-dist-worker/src/test/java/org/apache/bifromq/dist/worker/DistWorkerTest.java
+++ b/bifromq-dist/bifromq-dist-worker/src/test/java/org/apache/bifromq/dist/worker/DistWorkerTest.java
@@ -235,10 +235,11 @@ public abstract class DistWorkerTest {
.subBrokerManager(receiverManager)
.settingProvider(settingProvider)
.inlineFanoutThreshold(1)
+ .bootstrapDelay(Duration.ofSeconds(1))
.build();
rpcServer = rpcServerBuilder.build();
rpcServer.start();
- await().until(() -> BoundaryUtil.isValidSplitSet(storeClient.latestEffectiveRouter().keySet()));
+ await().forever().until(() -> BoundaryUtil.isValidSplitSet(storeClient.latestEffectiveRouter().keySet()));
log.info("Setup finished, and start testing");
}
diff --git a/bifromq-inbox/bifromq-inbox-coproc-proto/src/main/proto/inboxservice/InboxStoreCoProc.proto b/bifromq-inbox/bifromq-inbox-coproc-proto/src/main/proto/inboxservice/InboxStoreCoProc.proto
index 66fa081d..83ce4f89 100644
--- a/bifromq-inbox/bifromq-inbox-coproc-proto/src/main/proto/inboxservice/InboxStoreCoProc.proto
+++ b/bifromq-inbox/bifromq-inbox-coproc-proto/src/main/proto/inboxservice/InboxStoreCoProc.proto
@@ -196,9 +196,13 @@ message BatchFetchReply{
repeated Fetched result = 1;
}
+message MatchedRoute{
+ string topicFilter = 1; // the matched topic filter
+ uint64 incarnation = 2; // route incarnation
+}
// insert won't change version & lastActive timestamp
message SubMessagePack{
- map<string, uint64> matchedTopicFilters = 1; // key: topicFilter, value: route incarnation
+ repeated MatchedRoute matchedRoute = 1;
commontype.TopicMessagePack messages = 2;
}
@@ -219,9 +223,8 @@ message InsertResult{
NO_INBOX = 1;
}
message SubStatus{
- string topicFilter = 1;
- uint64 incarnation = 2;
- bool rejected = 3;
+ MatchedRoute matchedRoute = 1;
+ bool rejected = 2; // true if the route is outdated
}
Code code = 1;
repeated SubStatus result = 2;
diff --git a/bifromq-inbox/bifromq-inbox-server/src/main/java/org/apache/bifromq/inbox/server/InboxWriter.java b/bifromq-inbox/bifromq-inbox-server/src/main/java/org/apache/bifromq/inbox/server/InboxWriter.java
index 941a3865..77c0d428 100644
--- a/bifromq-inbox/bifromq-inbox-server/src/main/java/org/apache/bifromq/inbox/server/InboxWriter.java
+++ b/bifromq-inbox/bifromq-inbox-server/src/main/java/org/apache/bifromq/inbox/server/InboxWriter.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.inbox.server;
@@ -22,6 +22,15 @@ package org.apache.bifromq.inbox.server;
import static org.apache.bifromq.base.util.CompletableFutureUtil.unwrap;
import static org.apache.bifromq.plugin.subbroker.TypeUtil.toResult;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import lombok.extern.slf4j.Slf4j;
import org.apache.bifromq.base.util.AsyncRetry;
import org.apache.bifromq.base.util.exception.RetryTimeoutException;
import org.apache.bifromq.basekv.client.exception.BadVersionException;
@@ -34,6 +43,7 @@ import org.apache.bifromq.inbox.rpc.proto.SendRequest;
import org.apache.bifromq.inbox.server.scheduler.IInboxInsertScheduler;
import org.apache.bifromq.inbox.storage.proto.InsertRequest;
import org.apache.bifromq.inbox.storage.proto.InsertResult;
+import org.apache.bifromq.inbox.storage.proto.MatchedRoute;
import org.apache.bifromq.inbox.storage.proto.SubMessagePack;
import org.apache.bifromq.plugin.subbroker.DeliveryPack;
import org.apache.bifromq.plugin.subbroker.DeliveryReply;
@@ -41,14 +51,6 @@ import org.apache.bifromq.plugin.subbroker.DeliveryResult;
import org.apache.bifromq.sysprops.props.DataPlaneMaxBurstLatencyMillis;
import org.apache.bifromq.type.MatchInfo;
import org.apache.bifromq.type.TopicMessagePack;
-import org.apache.bifromq.util.TopicUtil;
-import java.time.Duration;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import lombok.extern.slf4j.Slf4j;
@Slf4j
class InboxWriter implements InboxWriterPipeline.ISendRequestHandler {
@@ -62,7 +64,7 @@ class InboxWriter implements InboxWriterPipeline.ISendRequestHandler {
@Override
public CompletableFuture<SendReply> handle(SendRequest request) {
- Map<TenantInboxInstance, List<MatchInfo>> matchInfosByInbox = new HashMap<>();
+ Map<TenantInboxInstance, Map<MatchedRoute, MatchInfo>> matchInfosByInbox = new HashMap<>();
Map<TenantInboxInstance, List<SubMessagePack>> subMsgPacksByInbox = new HashMap<>();
// break DeliveryPack into SubMessagePack by each TenantInboxInstance
for (String tenantId : request.getRequest().getPackageMap().keySet()) {
@@ -71,11 +73,15 @@ class InboxWriter implements InboxWriterPipeline.ISendRequestHandler {
Map<TenantInboxInstance, SubMessagePack.Builder> subMsgPackByInbox = new HashMap<>();
for (MatchInfo matchInfo : pack.getMatchInfoList()) {
TenantInboxInstance tenantInboxInstance = TenantInboxInstance.from(tenantId, matchInfo);
- matchInfosByInbox.computeIfAbsent(tenantInboxInstance, k -> new LinkedList<>()).add(matchInfo);
+ MatchedRoute matchedRoute = MatchedRoute.newBuilder()
+ .setTopicFilter(matchInfo.getMatcher().getMqttTopicFilter())
+ .setIncarnation(matchInfo.getIncarnation())
+ .build();
+ matchInfosByInbox.computeIfAbsent(tenantInboxInstance, k -> new HashMap<>())
+ .put(matchedRoute, matchInfo);
subMsgPackByInbox.computeIfAbsent(tenantInboxInstance,
k -> SubMessagePack.newBuilder().setMessages(topicMessagePack))
- .putMatchedTopicFilters(matchInfo.getMatcher().getMqttTopicFilter(),
- matchInfo.getIncarnation());
+ .addMatchedRoute(matchedRoute);
}
for (TenantInboxInstance tenantInboxInstance : subMsgPackByInbox.keySet()) {
subMsgPacksByInbox.computeIfAbsent(tenantInboxInstance, k -> new LinkedList<>())
@@ -127,20 +133,21 @@ class InboxWriter implements InboxWriterPipeline.ISendRequestHandler {
Map<String, Map<MatchInfo, DeliveryResult.Code>> tenantMatchResultMap = new HashMap<>();
int i = 0;
for (TenantInboxInstance tenantInboxInstance : subMsgPacksByInbox.keySet()) {
- String receiverId = tenantInboxInstance.receiverId();
+ Map<MatchedRoute, MatchInfo> matchedRoutesMap = matchInfosByInbox.get(tenantInboxInstance);
InsertResult result = replyFutures.get(i++).join();
Map<MatchInfo, DeliveryResult.Code> matchResultMap =
tenantMatchResultMap.computeIfAbsent(tenantInboxInstance.tenantId(), k -> new HashMap<>());
switch (result.getCode()) {
- case OK -> result.getResultList().forEach(insertionResult -> {
- DeliveryResult.Code code =
- insertionResult.getRejected() ? DeliveryResult.Code.NO_SUB : DeliveryResult.Code.OK;
- matchResultMap.putIfAbsent(MatchInfo.newBuilder().setReceiverId(receiverId)
- .setMatcher(TopicUtil.from(insertionResult.getTopicFilter()))
- .setIncarnation(insertionResult.getIncarnation()).build(), code);
- });
+ case OK -> {
+ Function<MatchedRoute, DeliveryResult.Code> resultFinder =
+ getFinalResultFinder(result.getResultList());
+ for (MatchedRoute matchedRoute : matchedRoutesMap.keySet()) {
+ matchResultMap.putIfAbsent(matchedRoutesMap.get(matchedRoute),
+ resultFinder.apply(matchedRoute));
+ }
+ }
case NO_INBOX -> {
- for (MatchInfo matchInfo : matchInfosByInbox.get(tenantInboxInstance)) {
+ for (MatchInfo matchInfo : matchedRoutesMap.values()) {
matchResultMap.putIfAbsent(matchInfo, DeliveryResult.Code.NO_RECEIVER);
}
}
@@ -155,4 +162,44 @@ class InboxWriter implements InboxWriterPipeline.ISendRequestHandler {
.build()).build();
}));
}
+
+ private Function<MatchedRoute, DeliveryResult.Code> getFinalResultFinder(List<InsertResult.SubStatus> subStatuses) {
+ Function<MatchedRoute, DeliveryResult.Code> resultFinder = getResultFinder(subStatuses);
+ return matchedRoute -> {
+ DeliveryResult.Code code = resultFinder.apply(matchedRoute);
+ if (code == null) {
+ // incompleted result from coproc
+ log.warn("MatchedRoute {} is missing in result", matchedRoute);
+ return DeliveryResult.Code.NO_SUB;
+ }
+ return code;
+ };
+ }
+
+ private Function<MatchedRoute, DeliveryResult.Code> getResultFinder(
+ List<InsertResult.SubStatus> subStatuses) {
+ if (subStatuses.size() == 1) {
+ InsertResult.SubStatus onlyStatus = subStatuses.get(0);
+ return matchedRoute -> {
+ if (matchedRoute.equals(onlyStatus.getMatchedRoute())) {
+ return onlyStatus.getRejected() ? DeliveryResult.Code.NO_SUB : DeliveryResult.Code.OK;
+ }
+ return null;
+ };
+ } else if (subStatuses.size() < 10) {
+ return matchedRoute -> {
+ for (InsertResult.SubStatus status : subStatuses) {
+ if (status.getMatchedRoute().equals(matchedRoute)) {
+ return status.getRejected() ? DeliveryResult.Code.NO_SUB : DeliveryResult.Code.OK;
+ }
+ }
+ return null;
+ };
+ } else {
+ Map<MatchedRoute, DeliveryResult.Code> resultMap = subStatuses.stream()
+ .collect(Collectors.toMap(InsertResult.SubStatus::getMatchedRoute,
+ e -> e.getRejected() ? DeliveryResult.Code.NO_SUB : DeliveryResult.Code.OK));
+ return resultMap::get;
+ }
+ }
}
diff --git a/bifromq-inbox/bifromq-inbox-server/src/test/java/org/apache/bifromq/inbox/server/InboxServiceTest.java b/bifromq-inbox/bifromq-inbox-server/src/test/java/org/apache/bifromq/inbox/server/InboxServiceTest.java
index 7306ea6e..bf560d9d 100644
--- a/bifromq-inbox/bifromq-inbox-server/src/test/java/org/apache/bifromq/inbox/server/InboxServiceTest.java
+++ b/bifromq-inbox/bifromq-inbox-server/src/test/java/org/apache/bifromq/inbox/server/InboxServiceTest.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.inbox.server;
@@ -26,15 +26,6 @@ import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
-import org.apache.bifromq.baserpc.client.IRPCClient;
-import org.apache.bifromq.baserpc.server.IRPCServer;
-import org.apache.bifromq.baserpc.server.RPCServerBuilder;
-import org.apache.bifromq.baserpc.trafficgovernor.IRPCServiceTrafficService;
-import org.apache.bifromq.plugin.eventcollector.IEventCollector;
-import org.apache.bifromq.plugin.settingprovider.ISettingProvider;
-import org.apache.bifromq.plugin.settingprovider.Setting;
-import org.apache.bifromq.retain.client.IRetainClient;
-import org.apache.bifromq.plugin.resourcethrottler.IResourceThrottler;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
@@ -51,11 +42,20 @@ import org.apache.bifromq.basekv.localengine.memory.InMemKVEngineConfigurator;
import org.apache.bifromq.basekv.metaservice.IBaseKVMetaService;
import org.apache.bifromq.basekv.store.option.KVRangeStoreOptions;
import org.apache.bifromq.basekv.utils.BoundaryUtil;
+import org.apache.bifromq.baserpc.client.IRPCClient;
+import org.apache.bifromq.baserpc.server.IRPCServer;
+import org.apache.bifromq.baserpc.server.RPCServerBuilder;
+import org.apache.bifromq.baserpc.trafficgovernor.IRPCServiceTrafficService;
import org.apache.bifromq.dist.client.IDistClient;
import org.apache.bifromq.dist.client.MatchResult;
import org.apache.bifromq.dist.client.UnmatchResult;
import org.apache.bifromq.inbox.client.IInboxClient;
import org.apache.bifromq.inbox.store.IInboxStore;
+import org.apache.bifromq.plugin.eventcollector.IEventCollector;
+import org.apache.bifromq.plugin.resourcethrottler.IResourceThrottler;
+import org.apache.bifromq.plugin.settingprovider.ISettingProvider;
+import org.apache.bifromq.plugin.settingprovider.Setting;
+import org.apache.bifromq.retain.client.IRetainClient;
import org.apache.bifromq.sessiondict.client.ISessionDictClient;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -144,6 +144,7 @@ public abstract class InboxServiceTest {
.tickerThreads(tickerThreads)
.bgTaskExecutor(bgTaskExecutor)
.detachTimeout(Duration.ofSeconds(2))
+ .bootstrapDelay(Duration.ofSeconds(1))
.build();
inboxServer = IInboxServer.builder()
.rpcServerBuilder(rpcServerBuilder)
@@ -153,7 +154,7 @@ public abstract class InboxServiceTest {
.build();
rpcServer = rpcServerBuilder.build();
rpcServer.start();
- await().until(() -> BoundaryUtil.isValidSplitSet(inboxStoreClient.latestEffectiveRouter().keySet()));
+ await().forever().until(() -> BoundaryUtil.isValidSplitSet(inboxStoreClient.latestEffectiveRouter().keySet()));
inboxClient.connState().filter(s -> s == IRPCClient.ConnState.READY).blockingFirst();
log.info("Setup finished, and start testing");
}
diff --git a/bifromq-inbox/bifromq-inbox-server/src/test/java/org/apache/bifromq/inbox/server/InboxWriterTest.java b/bifromq-inbox/bifromq-inbox-server/src/test/java/org/apache/bifromq/inbox/server/InboxWriterTest.java
index 1d811d3e..67a51d02 100644
--- a/bifromq-inbox/bifromq-inbox-server/src/test/java/org/apache/bifromq/inbox/server/InboxWriterTest.java
+++ b/bifromq-inbox/bifromq-inbox-server/src/test/java/org/apache/bifromq/inbox/server/InboxWriterTest.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.inbox.server;
@@ -25,16 +25,17 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
+import java.util.concurrent.CompletableFuture;
+import lombok.SneakyThrows;
import org.apache.bifromq.inbox.rpc.proto.SendReply;
import org.apache.bifromq.inbox.rpc.proto.SendRequest;
import org.apache.bifromq.inbox.server.scheduler.IInboxInsertScheduler;
import org.apache.bifromq.inbox.storage.proto.InsertRequest;
import org.apache.bifromq.inbox.storage.proto.InsertResult;
+import org.apache.bifromq.inbox.storage.proto.MatchedRoute;
import org.apache.bifromq.plugin.subbroker.DeliveryReply;
import org.apache.bifromq.plugin.subbroker.DeliveryResult;
import org.apache.bifromq.plugin.subbroker.DeliveryResults;
-import java.util.concurrent.CompletableFuture;
-import lombok.SneakyThrows;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.testng.annotations.AfterMethod;
@@ -86,8 +87,10 @@ public class InboxWriterTest {
InsertResult.newBuilder()
.addResult(InsertResult.SubStatus.newBuilder()
.setRejected(true)
- .setIncarnation(1L)
- .setTopicFilter("/foo/+")
+ .setMatchedRoute(MatchedRoute.newBuilder()
+ .setIncarnation(1L)
+ .setTopicFilter("/foo/+")
+ .build())
.build())
.setCode(InsertResult.Code.OK)
.build()));
@@ -103,8 +106,10 @@ public class InboxWriterTest {
.setCode(InsertResult.Code.OK)
.addResult(InsertResult.SubStatus.newBuilder()
.setRejected(false)
- .setTopicFilter("/foo/+")
- .setIncarnation(1L)
+ .setMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter("/foo/+")
+ .setIncarnation(1L)
+ .build())
.build())
.build()));
SendRequest request = sendRequest();
diff --git a/bifromq-inbox/bifromq-inbox-store/src/main/java/org/apache/bifromq/inbox/store/InboxStoreCoProc.java b/bifromq-inbox/bifromq-inbox-store/src/main/java/org/apache/bifromq/inbox/store/InboxStoreCoProc.java
index 3a019d80..383e06ac 100644
--- a/bifromq-inbox/bifromq-inbox-store/src/main/java/org/apache/bifromq/inbox/store/InboxStoreCoProc.java
+++ b/bifromq-inbox/bifromq-inbox-store/src/main/java/org/apache/bifromq/inbox/store/InboxStoreCoProc.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.inbox.store;
@@ -116,6 +116,7 @@ import org.apache.bifromq.inbox.storage.proto.InboxVersion;
import org.apache.bifromq.inbox.storage.proto.InsertRequest;
import org.apache.bifromq.inbox.storage.proto.InsertResult;
import org.apache.bifromq.inbox.storage.proto.LWT;
+import org.apache.bifromq.inbox.storage.proto.MatchedRoute;
import org.apache.bifromq.inbox.storage.proto.SubMessagePack;
import org.apache.bifromq.inbox.store.delay.DelayTaskRunner;
import org.apache.bifromq.inbox.store.delay.ExpireInboxTask;
@@ -966,39 +967,48 @@ final class InboxStoreCoProc implements IKVRangeCoProc {
List<SubMessage> bufferMsgList = new ArrayList<>();
Set<InsertResult.SubStatus> insertResults = new HashSet<>();
for (SubMessagePack messagePack : params.getMessagePackList()) {
- Map<String, Long> matchedTopicFilters = messagePack.getMatchedTopicFiltersMap();
Map<String, TopicFilterOption> qos0TopicFilters = new HashMap<>();
Map<String, TopicFilterOption> qos1TopicFilters = new HashMap<>();
Map<String, TopicFilterOption> qos2TopicFilters = new HashMap<>();
TopicMessagePack topicMsgPack = messagePack.getMessages();
- for (String matchedTopicFilter : matchedTopicFilters.keySet()) {
- long matchedIncarnation = matchedTopicFilters.get(matchedTopicFilter);
- TopicFilterOption tfOption = metadata.getTopicFiltersMap().get(matchedTopicFilter);
+ for (MatchedRoute matchedRoute : messagePack.getMatchedRouteList()) {
+ long matchedIncarnation = matchedRoute.getIncarnation();
+ TopicFilterOption tfOption = metadata.getTopicFiltersMap().get(matchedRoute.getTopicFilter());
if (tfOption == null) {
- insertResults.add(
- InsertResult.SubStatus.newBuilder().setTopicFilter(matchedTopicFilter)
- .setIncarnation(matchedIncarnation).setRejected(true).build());
+ insertResults.add(InsertResult.SubStatus.newBuilder()
+ .setMatchedRoute(matchedRoute)
+ .setRejected(true)
+ .build());
} else {
if (tfOption.getIncarnation() > matchedIncarnation) {
// messages from old sub incarnation
log.debug("Receive message from previous subscription: topicFilter={}, inc={}, prevInc={}",
- matchedTopicFilter, tfOption.getIncarnation(), matchedIncarnation);
+ matchedRoute, tfOption.getIncarnation(), matchedIncarnation);
+ insertResults.add(InsertResult.SubStatus.newBuilder()
+ .setMatchedRoute(matchedRoute)
+ .setRejected(true)
+ .build());
+ } else {
+ // messages from current incarnation
+ insertResults.add(InsertResult.SubStatus.newBuilder()
+ .setMatchedRoute(matchedRoute)
+ .setRejected(false)
+ .build());
}
switch (tfOption.getQos()) {
- case AT_MOST_ONCE -> qos0TopicFilters.put(matchedTopicFilter, tfOption);
- case AT_LEAST_ONCE -> qos1TopicFilters.put(matchedTopicFilter, tfOption);
- case EXACTLY_ONCE -> qos2TopicFilters.put(matchedTopicFilter, tfOption);
+ case AT_MOST_ONCE -> qos0TopicFilters.put(matchedRoute.getTopicFilter(), tfOption);
+ case AT_LEAST_ONCE -> qos1TopicFilters.put(matchedRoute.getTopicFilter(), tfOption);
+ case EXACTLY_ONCE -> qos2TopicFilters.put(matchedRoute.getTopicFilter(), tfOption);
default -> {
// never happens
}
}
- insertResults.add(InsertResult.SubStatus.newBuilder()
- .setTopicFilter(matchedTopicFilter)
- .setIncarnation(matchedIncarnation)
- .setRejected(false)
- .build());
}
}
+ if (qos0TopicFilters.isEmpty() && qos1TopicFilters.isEmpty() && qos2TopicFilters.isEmpty()) {
+ // no matched topic filter, skip this message pack
+ continue;
+ }
String topic = topicMsgPack.getTopic();
for (TopicMessagePack.PublisherPack publisherPack : topicMsgPack.getMessageList()) {
for (Message message : publisherPack.getMessageList()) {
diff --git a/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/InboxInsertTest.java b/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/InboxInsertTest.java
index 2d08a10a..e70fe507 100644
--- a/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/InboxInsertTest.java
+++ b/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/InboxInsertTest.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.inbox.store;
@@ -39,6 +39,7 @@ import org.apache.bifromq.inbox.storage.proto.InboxMessage;
import org.apache.bifromq.inbox.storage.proto.InboxVersion;
import org.apache.bifromq.inbox.storage.proto.InsertRequest;
import org.apache.bifromq.inbox.storage.proto.InsertResult;
+import org.apache.bifromq.inbox.storage.proto.MatchedRoute;
import org.apache.bifromq.inbox.storage.proto.SubMessagePack;
import org.apache.bifromq.plugin.eventcollector.inboxservice.Overflowed;
import org.apache.bifromq.type.ClientInfo;
@@ -61,7 +62,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 1L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(1L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(message(AT_MOST_ONCE, "hello"))
@@ -86,6 +90,147 @@ public class InboxInsertTest extends InboxStoreTest {
assertEquals(commitCode, BatchCommitReply.Code.NO_INBOX);
}
+
+ @Test(groups = "integration")
+ public void insertWithUnmatchedTopicFilterRejected() {
+ long now = 0;
+ String tenantId = "tenantId-" + System.nanoTime();
+ String inboxId = "inboxId-" + System.nanoTime();
+ long incarnation = System.nanoTime();
+ // do not create any subscription, so all matched topic filters will be unmatched
+ ClientInfo client = ClientInfo.newBuilder().setTenantId(tenantId).build();
+ BatchAttachRequest.Params attachParams = BatchAttachRequest.Params.newBuilder()
+ .setInboxId(inboxId)
+ .setIncarnation(incarnation)
+ .setExpirySeconds(2)
+ .setLimit(10)
+ .setClient(client)
+ .setNow(now)
+ .build();
+ requestAttach(attachParams).get(0);
+
+ String unmatchedTF = "/not/subscribed";
+ TopicMessagePack.PublisherPack msg = message(QoS.AT_MOST_ONCE, "hello-unmatched");
+
+ InsertResult insertResult = requestInsert(InsertRequest.newBuilder()
+ .setTenantId(tenantId)
+ .setInboxId(inboxId)
+ .setIncarnation(incarnation)
+ .addMessagePack(SubMessagePack.newBuilder()
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(unmatchedTF)
+ .setIncarnation(1L)
+ .build())
+ .setMessages(TopicMessagePack.newBuilder()
+ .setTopic(unmatchedTF)
+ .addMessage(msg)
+ .build())
+ .build())
+ .build()).get(0);
+
+ // insert is ignored because no subscription matches the topic filter
+ assertEquals(insertResult.getCode(), InsertResult.Code.OK);
+ assertEquals(insertResult.getResultCount(), 1);
+ assertEquals(insertResult.getResult(0).getMatchedRoute().getTopicFilter(), unmatchedTF);
+ assertEquals(insertResult.getResult(0).getMatchedRoute().getIncarnation(), 1L);
+ assertTrue(insertResult.getResult(0).getRejected());
+
+ // no messages should be fetched
+ Fetched fetched = requestFetch(BatchFetchRequest.Params.newBuilder()
+ .setTenantId(tenantId)
+ .setInboxId(inboxId)
+ .setIncarnation(incarnation)
+ .setMaxFetch(10)
+ .build()).get(0);
+ assertEquals(fetched.getQos0MsgCount(), 0);
+ assertEquals(fetched.getSendBufferMsgCount(), 0);
+ }
+
+ @Test(groups = "integration")
+ public void insertWithOldAndCurrentIncarnationMixed() {
+ long now = 0;
+ String tenantId = "tenantId-" + System.nanoTime();
+ String inboxId = "inboxId-" + System.nanoTime();
+ long incarnation = System.nanoTime();
+ String topicFilter = "/a/b/c";
+
+ ClientInfo client = ClientInfo.newBuilder().setTenantId(tenantId).build();
+ InboxVersion inboxVersion = requestAttach(BatchAttachRequest.Params.newBuilder()
+ .setInboxId(inboxId)
+ .setIncarnation(incarnation)
+ .setExpirySeconds(2)
+ .setLimit(10)
+ .setClient(client)
+ .setNow(now)
+ .build()).get(0);
+
+ requestSub(BatchSubRequest.Params.newBuilder()
+ .setTenantId(tenantId)
+ .setInboxId(inboxId)
+ .setVersion(inboxVersion)
+ .setTopicFilter(topicFilter)
+ .setOption(TopicFilterOption.newBuilder()
+ .setIncarnation(1L)
+ .setQos(QoS.AT_MOST_ONCE)
+ .build())
+ .setMaxTopicFilters(100)
+ .setNow(now)
+ .build());
+
+ TopicMessagePack.PublisherPack msg1 = message(QoS.AT_MOST_ONCE, "keep-me-1");
+ TopicMessagePack.PublisherPack msg2 = message(QoS.AT_MOST_ONCE, "keep-me-2");
+
+ // same topicFilter, same package with 2 matched: one old (0), one current (1)
+ InsertResult insertResult = requestInsert(InsertRequest.newBuilder()
+ .setTenantId(tenantId)
+ .setInboxId(inboxId)
+ .setIncarnation(incarnation)
+ .addMessagePack(SubMessagePack.newBuilder()
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(0L) // old -> rejected=true
+ .build())
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(1L) // matched -> rejected=false
+ .build())
+ .setMessages(TopicMessagePack.newBuilder()
+ .setTopic(topicFilter)
+ .addMessage(msg1)
+ .addMessage(msg2)
+ .build())
+ .build())
+ .build()).get(0);
+
+ assertEquals(insertResult.getCode(), InsertResult.Code.OK);
+ boolean oldRejected = false;
+ boolean currAccepted = false;
+ for (InsertResult.SubStatus s : insertResult.getResultList()) {
+ if (s.getMatchedRoute().getTopicFilter().equals(topicFilter)
+ && s.getMatchedRoute().getIncarnation() == 0L) {
+ assertTrue(s.getRejected());
+ oldRejected = true;
+ }
+ if (s.getMatchedRoute().getTopicFilter().equals(topicFilter)
+ && s.getMatchedRoute().getIncarnation() == 1L) {
+ assertFalse(s.getRejected());
+ currAccepted = true;
+ }
+ }
+ assertTrue(oldRejected && currAccepted);
+
+ Fetched fetched = requestFetch(BatchFetchRequest.Params.newBuilder()
+ .setTenantId(tenantId)
+ .setInboxId(inboxId)
+ .setIncarnation(incarnation)
+ .setMaxFetch(10)
+ .build()).get(0);
+
+ assertEquals(fetched.getQos0MsgCount(), 2);
+ assertEquals(fetched.getQos0Msg(0).getMsg().getMessage(), msg1.getMessage(0));
+ assertEquals(fetched.getQos0Msg(1).getMsg().getMessage(), msg2.getMessage(0));
+ }
+
protected void fetchWithoutStartAfter(QoS qos) {
long now = 0;
String tenantId = "tenantId-" + System.nanoTime();
@@ -120,7 +265,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 1L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(1L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(msg1)
@@ -129,8 +277,8 @@ public class InboxInsertTest extends InboxStoreTest {
.build())
.build()).get(0);
assertEquals(insertResult.getCode(), InsertResult.Code.OK);
- assertEquals(insertResult.getResult(0).getTopicFilter(), topicFilter);
- assertEquals(insertResult.getResult(0).getIncarnation(), 1L);
+ assertEquals(insertResult.getResult(0).getMatchedRoute().getTopicFilter(), topicFilter);
+ assertEquals(insertResult.getResult(0).getMatchedRoute().getIncarnation(), 1L);
Fetched fetched = requestFetch(
BatchFetchRequest.Params.newBuilder()
@@ -190,7 +338,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 1L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(1L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(msg1)
@@ -199,8 +350,8 @@ public class InboxInsertTest extends InboxStoreTest {
.build())
.build()).get(0);
assertEquals(insertResult.getCode(), InsertResult.Code.OK);
- assertEquals(insertResult.getResult(0).getTopicFilter(), topicFilter);
- assertEquals(insertResult.getResult(0).getIncarnation(), 1L);
+ assertEquals(insertResult.getResult(0).getMatchedRoute().getTopicFilter(), topicFilter);
+ assertEquals(insertResult.getResult(0).getMatchedRoute().getIncarnation(), 1L);
Fetched fetched = requestFetch(BatchFetchRequest.Params.newBuilder()
.setTenantId(tenantId)
@@ -270,7 +421,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 0L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(0L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(msg1)
@@ -284,7 +438,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 0L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(0L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(msg4)
@@ -386,7 +543,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 0L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(0L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(msg1)
@@ -504,7 +664,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 0L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(0L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(msg1)
@@ -518,7 +681,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 0L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(0L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(msg4)
@@ -587,7 +753,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 0L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(0L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(msg0)
@@ -599,7 +768,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 0L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(0L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(msg1)
@@ -611,7 +783,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 0L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(0L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(msg2)
@@ -646,7 +821,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 0L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(0L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(msg0)
@@ -715,7 +893,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 0L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(0L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(msg0)
@@ -727,7 +908,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 0L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(0L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(msg1)
@@ -755,7 +939,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 0L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(0L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(msg0)
@@ -824,7 +1011,10 @@ public class InboxInsertTest extends InboxStoreTest {
.setInboxId(inboxId)
.setIncarnation(incarnation)
.addMessagePack(SubMessagePack.newBuilder()
- .putMatchedTopicFilters(topicFilter, 0L)
+ .addMatchedRoute(MatchedRoute.newBuilder()
+ .setTopicFilter(topicFilter)
+ .setIncarnation(0L)
+ .build())
.setMessages(TopicMessagePack.newBuilder()
.setTopic(topicFilter)
.addMessage(msg0)
diff --git a/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/InboxStoreTest.java b/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/InboxStoreTest.java
index 73f23877..fdf2d27e 100644
--- a/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/InboxStoreTest.java
+++ b/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/InboxStoreTest.java
@@ -33,15 +33,6 @@ import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
-import org.apache.bifromq.baserpc.client.IConnectable;
-import org.apache.bifromq.baserpc.server.IRPCServer;
-import org.apache.bifromq.baserpc.server.RPCServerBuilder;
-import org.apache.bifromq.baserpc.trafficgovernor.IRPCServiceTrafficService;
-import org.apache.bifromq.plugin.eventcollector.IEventCollector;
-import org.apache.bifromq.plugin.settingprovider.ISettingProvider;
-import org.apache.bifromq.plugin.settingprovider.Setting;
-import org.apache.bifromq.retain.client.IRetainClient;
-import org.apache.bifromq.plugin.resourcethrottler.IResourceThrottler;
import com.google.protobuf.ByteString;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.Meter;
@@ -87,6 +78,10 @@ import org.apache.bifromq.basekv.store.proto.ROCoProcInput;
import org.apache.bifromq.basekv.store.proto.RWCoProcInput;
import org.apache.bifromq.basekv.store.proto.ReplyCode;
import org.apache.bifromq.basekv.utils.BoundaryUtil;
+import org.apache.bifromq.baserpc.client.IConnectable;
+import org.apache.bifromq.baserpc.server.IRPCServer;
+import org.apache.bifromq.baserpc.server.RPCServerBuilder;
+import org.apache.bifromq.baserpc.trafficgovernor.IRPCServiceTrafficService;
import org.apache.bifromq.dist.client.IDistClient;
import org.apache.bifromq.inbox.client.IInboxClient;
import org.apache.bifromq.inbox.storage.proto.BatchAttachRequest;
@@ -117,6 +112,11 @@ import org.apache.bifromq.inbox.storage.proto.InsertRequest;
import org.apache.bifromq.inbox.storage.proto.InsertResult;
import org.apache.bifromq.inbox.storage.proto.Replica;
import org.apache.bifromq.metrics.TenantMetric;
+import org.apache.bifromq.plugin.eventcollector.IEventCollector;
+import org.apache.bifromq.plugin.resourcethrottler.IResourceThrottler;
+import org.apache.bifromq.plugin.settingprovider.ISettingProvider;
+import org.apache.bifromq.plugin.settingprovider.Setting;
+import org.apache.bifromq.retain.client.IRetainClient;
import org.apache.bifromq.sessiondict.client.ISessionDictClient;
import org.apache.bifromq.type.ClientInfo;
import org.apache.bifromq.type.Message;
@@ -208,7 +208,7 @@ abstract class InboxStoreTest {
rpcServer.start();
storeClient.connState().filter(connState -> connState == IConnectable.ConnState.READY).blockingFirst();
- await().until(() -> BoundaryUtil.isValidSplitSet(storeClient.latestEffectiveRouter().keySet()));
+ await().forever().until(() -> BoundaryUtil.isValidSplitSet(storeClient.latestEffectiveRouter().keySet()));
log.info("Setup finished, and start testing");
}
@@ -232,6 +232,7 @@ abstract class InboxStoreTest {
.bgTaskExecutor(bgTaskExecutor)
.detachTimeout(Duration.ofSeconds(1))
.gcInterval(Duration.ofSeconds(1))
+ .bootstrapDelay(Duration.ofSeconds(1))
.build();
rpcServer = rpcServerBuilder.build();
}
diff --git a/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/LoadExistingTest.java b/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/LoadExistingTest.java
index 12b10a96..b4911cb8 100644
--- a/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/LoadExistingTest.java
+++ b/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/LoadExistingTest.java
@@ -59,7 +59,7 @@ public class LoadExistingTest extends InboxStoreTest {
InboxVersion inboxVersion = requestAttach(attachParams).get(0);
restartStoreServer();
- await().until(() -> BoundaryUtil.isValidSplitSet(storeClient.latestEffectiveRouter().keySet()));
+ await().forever().until(() -> BoundaryUtil.isValidSplitSet(storeClient.latestEffectiveRouter().keySet()));
when(sessionDictClient.exist(any())).thenReturn(CompletableFuture.completedFuture(OnlineCheckResult.NOT_EXISTS));
ArgumentCaptor<DeleteRequest> deleteCaptor = ArgumentCaptor.forClass(DeleteRequest.class);
verify(inboxClient, timeout(10000)).delete(deleteCaptor.capture());
diff --git a/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/LoadSubStatsTest.java b/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/LoadSubStatsTest.java
index 262bb628..1609614d 100644
--- a/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/LoadSubStatsTest.java
+++ b/bifromq-inbox/bifromq-inbox-store/src/test/java/org/apache/bifromq/inbox/store/LoadSubStatsTest.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.inbox.store;
@@ -73,7 +73,7 @@ public class LoadSubStatsTest extends InboxStoreTest {
restartStoreServer();
- await().until(() -> BoundaryUtil.isValidSplitSet(storeClient.latestEffectiveRouter().keySet()));
+ await().forever().until(() -> BoundaryUtil.isValidSplitSet(storeClient.latestEffectiveRouter().keySet()));
Gauge newSubCountGauge = getSubCountGauge(tenantId);
Gauge newPSessionGauge = getPSessionGauge(tenantId);
Gauge newPSessionSpaceGauge = getPSessionSpaceGauge(tenantId);
diff --git a/bifromq-mqtt/bifromq-mqtt-server/src/test/java/org/apache/bifromq/mqtt/integration/MQTTTest.java b/bifromq-mqtt/bifromq-mqtt-server/src/test/java/org/apache/bifromq/mqtt/integration/MQTTTest.java
index 855fe427..a1e593dc 100644
--- a/bifromq-mqtt/bifromq-mqtt-server/src/test/java/org/apache/bifromq/mqtt/integration/MQTTTest.java
+++ b/bifromq-mqtt/bifromq-mqtt-server/src/test/java/org/apache/bifromq/mqtt/integration/MQTTTest.java
@@ -184,6 +184,7 @@ public abstract class MQTTTest {
.resourceThrottler(resourceThrottler)
.tickerThreads(tickerThreads)
.bgTaskExecutor(bgTaskExecutor)
+ .bootstrapDelay(Duration.ofSeconds(1))
.storeOptions(new KVRangeStoreOptions()
.setDataEngineConfigurator(new InMemKVEngineConfigurator())
.setWalEngineConfigurator(new InMemKVEngineConfigurator()))
@@ -214,6 +215,7 @@ public abstract class MQTTTest {
.retainStoreClient(retainStoreKVStoreClient)
.tickerThreads(tickerThreads)
.bgTaskExecutor(bgTaskExecutor)
+ .bootstrapDelay(Duration.ofSeconds(1))
.storeOptions(new KVRangeStoreOptions()
.setDataEngineConfigurator(new InMemKVEngineConfigurator())
.setWalEngineConfigurator(new InMemKVEngineConfigurator()))
@@ -243,6 +245,7 @@ public abstract class MQTTTest {
.distWorkerClient(distWorkerStoreClient)
.tickerThreads(tickerThreads)
.bgTaskExecutor(bgTaskExecutor)
+ .bootstrapDelay(Duration.ofSeconds(1))
.storeOptions(new KVRangeStoreOptions()
.setDataEngineConfigurator(new InMemKVEngineConfigurator())
.setWalEngineConfigurator(new InMemKVEngineConfigurator()))
@@ -299,9 +302,9 @@ public abstract class MQTTTest {
.filter(state -> state == IRPCClient.ConnState.READY)
.firstElement()
.blockingSubscribe();
- await().until(() -> BoundaryUtil.isValidSplitSet(distWorkerStoreClient.latestEffectiveRouter().keySet()));
- await().until(() -> BoundaryUtil.isValidSplitSet(inboxStoreKVStoreClient.latestEffectiveRouter().keySet()));
- await().until(() -> BoundaryUtil.isValidSplitSet(retainStoreKVStoreClient.latestEffectiveRouter().keySet()));
+ await().forever().until(() -> BoundaryUtil.isValidSplitSet(distWorkerStoreClient.latestEffectiveRouter().keySet()));
+ await().forever().until(() -> BoundaryUtil.isValidSplitSet(inboxStoreKVStoreClient.latestEffectiveRouter().keySet()));
+ await().forever().until(() -> BoundaryUtil.isValidSplitSet(retainStoreKVStoreClient.latestEffectiveRouter().keySet()));
lenient().when(settingProvider.provide(any(), anyString()))
.thenAnswer(invocation -> {
Setting setting = invocation.getArgument(0);
diff --git a/bifromq-plugin/bifromq-plugin-setting-provider/src/main/java/org/apache/bifromq/plugin/settingprovider/Setting.java b/bifromq-plugin/bifromq-plugin-setting-provider/src/main/java/org/apache/bifromq/plugin/settingprovider/Setting.java
index a5961918..a6af28b2 100644
--- a/bifromq-plugin/bifromq-plugin-setting-provider/src/main/java/org/apache/bifromq/plugin/settingprovider/Setting.java
+++ b/bifromq-plugin/bifromq-plugin-setting-provider/src/main/java/org/apache/bifromq/plugin/settingprovider/Setting.java
@@ -28,10 +28,14 @@ import lombok.extern.slf4j.Slf4j;
*/
@Slf4j
public enum Setting {
- MQTT3Enabled(Boolean.class, val -> true, true), MQTT4Enabled(Boolean.class, val -> true, true),
- MQTT5Enabled(Boolean.class, val -> true, true), DebugModeEnabled(Boolean.class, val -> true, false),
- ForceTransient(Boolean.class, val -> true, false), ByPassPermCheckError(Boolean.class, val -> true, true),
- PayloadFormatValidationEnabled(Boolean.class, val -> true, true), RetainEnabled(Boolean.class, val -> true, true),
+ MQTT3Enabled(Boolean.class, val -> true, true),
+ MQTT4Enabled(Boolean.class, val -> true, true),
+ MQTT5Enabled(Boolean.class, val -> true, true),
+ DebugModeEnabled(Boolean.class, val -> true, false),
+ ForceTransient(Boolean.class, val -> true, false),
+ ByPassPermCheckError(Boolean.class, val -> true, true),
+ PayloadFormatValidationEnabled(Boolean.class, val -> true, true),
+ RetainEnabled(Boolean.class, val -> true, true),
WildcardSubscriptionEnabled(Boolean.class, val -> true, true),
SubscriptionIdentifierEnabled(Boolean.class, val -> true, true),
SharedSubscriptionEnabled(Boolean.class, val -> true, true),
diff --git a/bifromq-retain/bifromq-retain-store/src/test/java/org/apache/bifromq/retain/store/LoadMetadataTest.java b/bifromq-retain/bifromq-retain-store/src/test/java/org/apache/bifromq/retain/store/LoadMetadataTest.java
index f751fbaf..abdbe24c 100644
--- a/bifromq-retain/bifromq-retain-store/src/test/java/org/apache/bifromq/retain/store/LoadMetadataTest.java
+++ b/bifromq-retain/bifromq-retain-store/src/test/java/org/apache/bifromq/retain/store/LoadMetadataTest.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.retain.store;
@@ -22,9 +22,9 @@ package org.apache.bifromq.retain.store;
import static org.awaitility.Awaitility.await;
import static org.testng.Assert.assertNotSame;
-import org.apache.bifromq.basekv.utils.BoundaryUtil;
import io.micrometer.core.instrument.Gauge;
import java.time.Duration;
+import org.apache.bifromq.basekv.utils.BoundaryUtil;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -47,7 +47,7 @@ public class LoadMetadataTest extends RetainStoreTest {
Gauge retainCountGauge = getRetainCountGauge(tenantId);
restartStoreServer();
- await().until(() -> BoundaryUtil.isValidSplitSet(storeClient.latestEffectiveRouter().keySet()));
+ await().forever().until(() -> BoundaryUtil.isValidSplitSet(storeClient.latestEffectiveRouter().keySet()));
Gauge newSpaceUsageGauge = getSpaceUsageGauge(tenantId);
Gauge newRetainCountGauge = getRetainCountGauge(tenantId);
diff --git a/bifromq-retain/bifromq-retain-store/src/test/java/org/apache/bifromq/retain/store/RetainStoreTest.java b/bifromq-retain/bifromq-retain-store/src/test/java/org/apache/bifromq/retain/store/RetainStoreTest.java
index 926e78a0..4f792c2d 100644
--- a/bifromq-retain/bifromq-retain-store/src/test/java/org/apache/bifromq/retain/store/RetainStoreTest.java
+++ b/bifromq-retain/bifromq-retain-store/src/test/java/org/apache/bifromq/retain/store/RetainStoreTest.java
@@ -14,7 +14,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
- * under the License.
+ * under the License.
*/
package org.apache.bifromq.retain.store;
@@ -28,6 +28,25 @@ import static org.awaitility.Awaitility.await;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
+import com.google.protobuf.ByteString;
+import io.micrometer.core.instrument.Gauge;
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.Metrics;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicReference;
+import lombok.extern.slf4j.Slf4j;
import org.apache.bifromq.basecluster.AgentHostOptions;
import org.apache.bifromq.basecluster.IAgentHost;
import org.apache.bifromq.basecrdt.service.CRDTServiceOptions;
@@ -69,25 +88,6 @@ import org.apache.bifromq.retain.store.schema.KVSchemaUtil;
import org.apache.bifromq.type.ClientInfo;
import org.apache.bifromq.type.Message;
import org.apache.bifromq.type.TopicMessage;
-import com.google.protobuf.ByteString;
-import io.micrometer.core.instrument.Gauge;
-import io.micrometer.core.instrument.Meter;
-import io.micrometer.core.instrument.Metrics;
-import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.time.Duration;
-import java.util.Comparator;
-import java.util.Objects;
-import java.util.UUID;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.ThreadLocalRandom;
-import java.util.concurrent.atomic.AtomicReference;
-import lombok.extern.slf4j.Slf4j;
import org.mockito.MockitoAnnotations;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
@@ -143,7 +143,7 @@ public class RetainStoreTest {
.metaService(metaService).build();
buildStoreServer();
rpcServer.start();
- await().until(() -> BoundaryUtil.isValidSplitSet(storeClient.latestEffectiveRouter().keySet()));
+ await().forever().until(() -> BoundaryUtil.isValidSplitSet(storeClient.latestEffectiveRouter().keySet()));
log.info("Setup finished, and start testing");
}
@@ -158,6 +158,7 @@ public class RetainStoreTest {
.tickerThreads(tickerThreads)
.bgTaskExecutor(bgTaskExecutor)
.gcInterval(Duration.ofSeconds(60))
+ .bootstrapDelay(Duration.ofSeconds(1))
.build();
rpcServer = rpcServerBuilder.build();
}
diff --git a/build/build-bifromq-starter/conf/log4j2.xml b/build/build-bifromq-starter/conf/log4j2.xml
index a646e40a..c3fd5569 100644
--- a/build/build-bifromq-starter/conf/log4j2.xml
+++ b/build/build-bifromq-starter/conf/log4j2.xml
@@ -41,7 +41,7 @@
%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [%t] --- [%F:%L] %m - [id=%X{id},term=%X{term},state=%X{state},leader=%X{leader},f=%X{first},l=%X{last}],c=%X{commit},cfg=%X{config}]%n
]]></Property>
<Property name="balancerLogPattern"><![CDATA[
- %d{yyyy-MM-dd HH:mm:ss.SSS} %5p --- %m%n
+ %d{yyyy-MM-dd HH:mm:ss.SSS} %5p --- [%X{balancer}] %m%n
]]></Property>
</Properties>