This is an automated email from the ASF dual-hosted git repository.
popduke 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 41313fdde feat: make plugin FQN configs optional with
strict-or-graceful loading (#237)
41313fdde is described below
commit 41313fdde2486b4ff7e883c070592de3f09d8f19
Author: liaodongnian <[email protected]>
AuthorDate: Mon Mar 30 17:19:03 2026 +0800
feat: make plugin FQN configs optional with strict-or-graceful loading
(#237)
* feat(core): make plugin FQN configs optional with strict-or-graceful
loading
Make authProviderFQN, resourceThrottlerFQN, and settingProviderFQN in
standalone.yml optional to improve local dev/test ergonomics.
- If a *FQN field is explicitly set, treat it as a hard requirement:
BifroMQ will fail fast on startup if the plugin class is missing or fails
to initialize.
- If the field is omitted, apply auto-selection logic:
1. Discover all registered custom plugins for the extension point.
2. Sort by plugin key (byte-wise alphabetical) and pick the first.
3. Fall back to built-in default only if no custom plugins exist.
This provides a clear contract for production (explicit = strict)
while keeping local development simple and low-friction.
Fixes #236
Signed-off-by: liaodongnian <[email protected]>
---
.../plugin/authprovider/AuthProviderManager.java | 16 +-
.../authprovider/AuthProviderPluginException.java | 31 +++
.../authprovider/AuthProviderManagerTest.java | 82 +++++++-
.../ResourceThrottlerException.java | 31 +++
.../ResourceThrottlerManager.java | 17 +-
.../ResourceThrottlerManagerTest.java | 222 +++++++++++++++++++++
.../TenantResourceThrottlerManagerTest.java | 5 +-
.../settingprovider/SettingProviderException.java | 31 +++
.../settingprovider/SettingProviderManager.java | 21 +-
.../SettingProviderManagerTest.java | 108 ++++++++--
10 files changed, 514 insertions(+), 50 deletions(-)
diff --git
a/bifromq-plugin/bifromq-plugin-auth-provider-helper/src/main/java/org/apache/bifromq/plugin/authprovider/AuthProviderManager.java
b/bifromq-plugin/bifromq-plugin-auth-provider-helper/src/main/java/org/apache/bifromq/plugin/authprovider/AuthProviderManager.java
index a22f15066..84687b5e4 100644
---
a/bifromq-plugin/bifromq-plugin-auth-provider-helper/src/main/java/org/apache/bifromq/plugin/authprovider/AuthProviderManager.java
+++
b/bifromq-plugin/bifromq-plugin-auth-provider-helper/src/main/java/org/apache/bifromq/plugin/authprovider/AuthProviderManager.java
@@ -40,6 +40,7 @@ import
org.apache.bifromq.plugin.settingprovider.ISettingProvider;
import org.apache.bifromq.type.ClientInfo;
import io.micrometer.core.instrument.Timer;
import java.util.Map;
+import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
@@ -64,17 +65,22 @@ public class AuthProviderManager implements IAuthProvider,
AutoCloseable {
this.settingProvider = settingProvider;
this.eventCollector = eventCollector;
Map<String, IAuthProvider> availAuthProviders =
pluginMgr.getExtensions(IAuthProvider.class)
- .stream().collect(Collectors.toMap(e -> e.getClass().getName(), e
-> e));
+ .stream().collect(Collectors.toMap(e -> e.getClass().getName(), e
-> e,
+ (k,v) -> v, TreeMap::new));
if (availAuthProviders.isEmpty()) {
pluginLog.warn("No auth provider plugin available, use DEV ONLY
one instead");
delegate = new DevOnlyAuthProvider();
} else {
if (authProviderFQN == null) {
- pluginLog.warn("Auth provider plugin type not specified, use
DEV ONLY one instead");
- delegate = new DevOnlyAuthProvider();
+ if (availAuthProviders.size() > 1) {
+ pluginLog.info("Auth provider plugin type not specified,
use the first found");
+ }
+ String firstAuthProviderFQN =
availAuthProviders.keySet().iterator().next();
+ pluginLog.info("Auth provider plugin loaded: {}",
firstAuthProviderFQN);
+ delegate = availAuthProviders.get(firstAuthProviderFQN);
} else if (!availAuthProviders.containsKey(authProviderFQN)) {
- pluginLog.warn("Auth provider plugin type '{}' not found, use
DEV ONLY one instead", authProviderFQN);
- delegate = new DevOnlyAuthProvider();
+ pluginLog.warn("Auth provider plugin type '{}' not found, so
the system will shut down.", authProviderFQN);
+ throw new AuthProviderPluginException("Auth provider plugin
type '%s' not found, so the system will shut down.", authProviderFQN);
} else {
pluginLog.info("Auth provider plugin type: {}",
authProviderFQN);
delegate = availAuthProviders.get(authProviderFQN);
diff --git
a/bifromq-plugin/bifromq-plugin-auth-provider-helper/src/main/java/org/apache/bifromq/plugin/authprovider/AuthProviderPluginException.java
b/bifromq-plugin/bifromq-plugin-auth-provider-helper/src/main/java/org/apache/bifromq/plugin/authprovider/AuthProviderPluginException.java
new file mode 100644
index 000000000..42b404ecc
--- /dev/null
+++
b/bifromq-plugin/bifromq-plugin-auth-provider-helper/src/main/java/org/apache/bifromq/plugin/authprovider/AuthProviderPluginException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.plugin.authprovider;
+
+import org.pf4j.util.StringUtils;
+
+public class AuthProviderPluginException extends RuntimeException {
+
+ public AuthProviderPluginException(String message, Object... args) {
+ super(StringUtils.format(message, args));
+ }
+
+}
diff --git
a/bifromq-plugin/bifromq-plugin-auth-provider-helper/src/test/java/org/apache/bifromq/plugin/authprovider/AuthProviderManagerTest.java
b/bifromq-plugin/bifromq-plugin-auth-provider-helper/src/test/java/org/apache/bifromq/plugin/authprovider/AuthProviderManagerTest.java
index 04690bb67..377b24347 100644
---
a/bifromq-plugin/bifromq-plugin-auth-provider-helper/src/test/java/org/apache/bifromq/plugin/authprovider/AuthProviderManagerTest.java
+++
b/bifromq-plugin/bifromq-plugin-auth-provider-helper/src/test/java/org/apache/bifromq/plugin/authprovider/AuthProviderManagerTest.java
@@ -41,6 +41,7 @@ import
org.apache.bifromq.plugin.authprovider.type.MQTT5AuthResult;
import org.apache.bifromq.plugin.authprovider.type.MQTT5ExtendedAuthData;
import org.apache.bifromq.plugin.authprovider.type.MQTT5ExtendedAuthResult;
import org.apache.bifromq.plugin.authprovider.type.MQTTAction;
+import org.apache.bifromq.plugin.authprovider.type.Ok;
import org.apache.bifromq.plugin.authprovider.type.PubAction;
import org.apache.bifromq.plugin.authprovider.type.Reject;
import org.apache.bifromq.plugin.authprovider.type.SubAction;
@@ -52,6 +53,8 @@ import org.apache.bifromq.type.ClientInfo;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+
+import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import lombok.extern.slf4j.Slf4j;
@@ -133,18 +136,52 @@ public class AuthProviderManagerTest {
}
@Test
+ public void pluginNotSpecifiedWithSingleProvider() {
+ manager = new AuthProviderManager(null, pluginManager,
settingProvider, eventCollector);
+ when(mockProvider.auth(mockAuth3Data)).thenReturn(
+ CompletableFuture.completedFuture(MQTT3AuthResult.newBuilder()
+
.setReject(Reject.newBuilder().setCode(Reject.Code.BadPass).build()).build()));
+ MQTT3AuthResult result = manager.auth(mockAuth3Data).join();
+ assertEquals(result.getTypeCase(), MQTT3AuthResult.TypeCase.REJECT);
+ assertEquals(result.getReject().getCode(), Reject.Code.BadPass);
+ manager.close();
+ }
+
+ @Test
+ public void pluginNotSpecifiedWithMultipleProviders() {
+ IAuthProvider provider1 = new FirstTestAuthProvider();
+ IAuthProvider provider2 = new SecondTestAuthProvider();
+
+ when(pluginManager.getExtensions(IAuthProvider.class)).thenReturn(
+ Arrays.asList(provider1, provider2));
+ manager = new AuthProviderManager(null, pluginManager,
settingProvider, eventCollector);
+
+ MQTT3AuthResult result = manager.auth(mockAuth3Data).join();
+ // Deterministically selects the provider with lexicographically
smallest class name
+ assertEquals(result.getOk().getTenantId(), "FirstProvider");
+ manager.close();
+ }
+
+ @Test
+ public void pluginNotSpecifiedWithMultipleSortByKeyProviders() {
+ IAuthProvider provider1 = new FirstTestAuthProvider();
+ IAuthProvider provider2 = new SecondTestAuthProvider();
+
+ when(pluginManager.getExtensions(IAuthProvider.class)).thenReturn(
+ Arrays.asList(provider2, provider1));
+ manager = new AuthProviderManager(null, pluginManager,
settingProvider, eventCollector);
+
+ MQTT3AuthResult result = manager.auth(mockAuth3Data).join();
+ // Deterministically selects the provider with lexicographically
smallest class name
+ assertEquals(result.getOk().getTenantId(), "FirstProvider");
+ manager.close();
+ }
+
+
+ @Test(expectedExceptions = AuthProviderPluginException.class)
public void pluginNotFound() {
manager = new AuthProviderManager("Fake", pluginManager,
settingProvider, eventCollector);
- MQTT3AuthResult result = manager.auth(mockAuth3Data).join();
- assertEquals(result.getTypeCase(), MQTT3AuthResult.TypeCase.OK);
- assertEquals(result.getOk().getTenantId(), "DevOnly");
- boolean allow = manager.check(ClientInfo.getDefaultInstance(),
MQTTAction.newBuilder()
- .setSub(SubAction.getDefaultInstance()).build()).join();
- assertTrue(allow);
- allow = manager.check(ClientInfo.getDefaultInstance(),
MQTTAction.newBuilder()
- .setSub(SubAction.getDefaultInstance()).build()).join();
- assertTrue(allow);
manager.close();
}
@@ -404,4 +441,31 @@ public class AuthProviderManagerTest {
assertEquals(meterRegistry.find(CALL_FAIL_COUNTER).tag(TAG_METHOD,
"AuthProvider/check").counter().count(),
0);
}
+
+ static class FirstTestAuthProvider implements IAuthProvider {
+ @Override
+ public CompletableFuture<MQTT3AuthResult> auth(MQTT3AuthData authData)
{
+ return
CompletableFuture.completedFuture(MQTT3AuthResult.newBuilder()
+
.setOk(Ok.newBuilder().setTenantId("FirstProvider").build()).build());
+ }
+
+ @Override
+ public CompletableFuture<Boolean> check(ClientInfo client, MQTTAction
action) {
+ return CompletableFuture.completedFuture(true);
+ }
+ }
+
+ static class SecondTestAuthProvider implements IAuthProvider {
+ @Override
+ public CompletableFuture<MQTT3AuthResult> auth(MQTT3AuthData authData)
{
+ return
CompletableFuture.completedFuture(MQTT3AuthResult.newBuilder()
+
.setOk(Ok.newBuilder().setTenantId("SecondProvider").build()).build());
+ }
+
+ @Override
+ public CompletableFuture<Boolean> check(ClientInfo client, MQTTAction
action) {
+ return CompletableFuture.completedFuture(true);
+ }
+
+ }
}
diff --git
a/bifromq-plugin/bifromq-plugin-resource-throttler-helper/src/main/java/org/apache/bifromq/plugin/resourcethrottler/ResourceThrottlerException.java
b/bifromq-plugin/bifromq-plugin-resource-throttler-helper/src/main/java/org/apache/bifromq/plugin/resourcethrottler/ResourceThrottlerException.java
new file mode 100644
index 000000000..a637c06a6
--- /dev/null
+++
b/bifromq-plugin/bifromq-plugin-resource-throttler-helper/src/main/java/org/apache/bifromq/plugin/resourcethrottler/ResourceThrottlerException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.plugin.resourcethrottler;
+
+import org.pf4j.util.StringUtils;
+
+public class ResourceThrottlerException extends RuntimeException {
+
+ public ResourceThrottlerException(String message, Object... args) {
+ super(StringUtils.format(message, args));
+ }
+
+}
diff --git
a/bifromq-plugin/bifromq-plugin-resource-throttler-helper/src/main/java/org/apache/bifromq/plugin/resourcethrottler/ResourceThrottlerManager.java
b/bifromq-plugin/bifromq-plugin-resource-throttler-helper/src/main/java/org/apache/bifromq/plugin/resourcethrottler/ResourceThrottlerManager.java
index 9a3818130..f0197bb37 100644
---
a/bifromq-plugin/bifromq-plugin-resource-throttler-helper/src/main/java/org/apache/bifromq/plugin/resourcethrottler/ResourceThrottlerManager.java
+++
b/bifromq-plugin/bifromq-plugin-resource-throttler-helper/src/main/java/org/apache/bifromq/plugin/resourcethrottler/ResourceThrottlerManager.java
@@ -23,6 +23,7 @@ import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Timer;
import java.util.Map;
+import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@@ -41,18 +42,24 @@ public class ResourceThrottlerManager implements
IResourceThrottler, AutoCloseab
public ResourceThrottlerManager(String resourceThrottlerFQN, PluginManager
pluginMgr) {
Map<String, IResourceThrottler> availResourceThrottlers =
pluginMgr.getExtensions(IResourceThrottler.class).stream()
- .collect(Collectors.toMap(e -> e.getClass().getName(), e ->
e));
+ .collect(Collectors.toMap(e -> e.getClass().getName(), e -> e,
+ (k,v) -> v, TreeMap::new));
if (availResourceThrottlers.isEmpty()) {
pluginLog.warn("No resource throttler plugin available, use DEV
ONLY one instead");
delegate = new DevOnlyResourceThrottler();
} else {
if (resourceThrottlerFQN == null) {
- pluginLog.warn("Resource throttler type class not specified,
use DEV ONLY one instead");
- delegate = new DevOnlyResourceThrottler();
+ if (availResourceThrottlers.size() > 1) {
+ pluginLog.info("Resource throttler plugin type not
specified, use the first found");
+ }
+ String firstResourceThrottlerFQN =
availResourceThrottlers.keySet().iterator().next();
+ pluginLog.info("Resource throttler plugin loaded: {}",
firstResourceThrottlerFQN);
+ delegate =
availResourceThrottlers.get(firstResourceThrottlerFQN);
} else if
(!availResourceThrottlers.containsKey(resourceThrottlerFQN)) {
- pluginLog.warn("Resource throttler type '{}' not found, use
DEV ONLY one instead",
+ pluginLog.warn("Resource throttler type '{}' not found, so the
system will shut down.",
resourceThrottlerFQN);
- delegate = new DevOnlyResourceThrottler();
+ throw new ResourceThrottlerException("Resource throttler type
'%s' not found, so the system will shut down.",
+ resourceThrottlerFQN);
} else {
pluginLog.info("Resource throttler loaded: {}",
resourceThrottlerFQN);
delegate = availResourceThrottlers.get(resourceThrottlerFQN);
diff --git
a/bifromq-plugin/bifromq-plugin-resource-throttler-helper/src/test/java/org/apache/bifromq/plugin/resourcethrottler/ResourceThrottlerManagerTest.java
b/bifromq-plugin/bifromq-plugin-resource-throttler-helper/src/test/java/org/apache/bifromq/plugin/resourcethrottler/ResourceThrottlerManagerTest.java
new file mode 100644
index 000000000..39f7349b7
--- /dev/null
+++
b/bifromq-plugin/bifromq-plugin-resource-throttler-helper/src/test/java/org/apache/bifromq/plugin/resourcethrottler/ResourceThrottlerManagerTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.plugin.resourcethrottler;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Metrics;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.CompletableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.pf4j.PluginManager;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+@Slf4j
+public class ResourceThrottlerManagerTest {
+ @Mock
+ private PluginManager pluginManager;
+ @Mock
+ private IResourceThrottler mockResourceThrottler;
+ private MeterRegistry meterRegistry;
+ private final String tenantId = "testTenant";
+ private final TenantResourceType resourceType =
TenantResourceType.TotalConnections;
+ private ResourceThrottlerManager manager;
+ private AutoCloseable closeable;
+
+ @BeforeMethod
+ public void setup() {
+ meterRegistry = new SimpleMeterRegistry();
+ Metrics.globalRegistry.add(meterRegistry);
+ closeable = MockitoAnnotations.openMocks(this);
+ when(pluginManager.getExtensions(IResourceThrottler.class)).thenReturn(
+ Collections.singletonList(mockResourceThrottler));
+ }
+
+ @AfterMethod
+ public void tearDown() throws Exception {
+ closeable.close();
+ meterRegistry.clear();
+ Metrics.globalRegistry.clear();
+ }
+
+ @Test
+ public void devOnlyMode() {
+
when(pluginManager.getExtensions(IResourceThrottler.class)).thenReturn(Collections.emptyList());
+ manager = new ResourceThrottlerManager(null, pluginManager);
+ for (TenantResourceType type : TenantResourceType.values()) {
+ assertTrue(manager.hasResource(tenantId, type));
+ }
+ manager.close();
+ }
+
+ @Test
+ public void pluginSpecified() {
+ manager = new
ResourceThrottlerManager(mockResourceThrottler.getClass().getName(),
pluginManager);
+ when(mockResourceThrottler.hasResource(anyString(),
any(TenantResourceType.class))).thenReturn(false);
+ boolean hasResource = manager.hasResource(tenantId, resourceType);
+ assertFalse(hasResource);
+ manager.close();
+ }
+
+ @Test
+ public void pluginNotSpecifiedWithSingleProvider() {
+ manager = new ResourceThrottlerManager(null, pluginManager);
+ when(mockResourceThrottler.hasResource(anyString(),
any(TenantResourceType.class))).thenReturn(false);
+ boolean hasResource = manager.hasResource(tenantId, resourceType);
+ assertFalse(hasResource);
+ manager.close();
+ }
+
+ @Test
+ public void pluginNotSpecifiedWithMultipleProviders() {
+ IResourceThrottler provider1 = new FirstTestResourceThrottler();
+ IResourceThrottler provider2 = new SecondTestResourceThrottler();
+
+ when(pluginManager.getExtensions(IResourceThrottler.class)).thenReturn(
+ Arrays.asList(provider1, provider2));
+ manager = new ResourceThrottlerManager(null, pluginManager);
+
+ boolean hasResource = manager.hasResource(tenantId, resourceType);
+ // Should use one of the providers (order depends on TreeMap keySet
iteration)
+ assertTrue(hasResource);
+ manager.close();
+ }
+
+ @Test
+ public void pluginNotSpecifiedWithMultipleSortByKeyProviders() {
+ IResourceThrottler provider1 = new FirstTestResourceThrottler();
+ IResourceThrottler provider2 = new SecondTestResourceThrottler();
+
+ when(pluginManager.getExtensions(IResourceThrottler.class)).thenReturn(
+ Arrays.asList(provider2, provider1));
+ manager = new ResourceThrottlerManager(null, pluginManager);
+
+ boolean hasResource = manager.hasResource(tenantId, resourceType);
+ // Should use one of the providers (order depends on TreeMap keySet
iteration)
+ assertTrue(hasResource);
+ manager.close();
+ }
+
+ @Test(expectedExceptions = ResourceThrottlerException.class)
+ public void pluginNotFound() {
+ manager = new ResourceThrottlerManager("Fake", pluginManager);
+ manager.close();
+ }
+
+ @Test
+ public void hasResourceOK() {
+ manager = new
ResourceThrottlerManager(mockResourceThrottler.getClass().getName(),
pluginManager);
+ when(mockResourceThrottler.hasResource(anyString(),
any(TenantResourceType.class)))
+ .thenReturn(true);
+ boolean hasResource = manager.hasResource(tenantId, resourceType);
+ assertTrue(hasResource);
+ assertEquals(meterRegistry.find("call.exec.timer")
+ .tag("method", "ResourceThrottler/hasResource")
+ .timer()
+ .count(), 1);
+ assertEquals(meterRegistry.find("call.exec.fail.count")
+ .tag("method", "ResourceThrottler/hasResource")
+ .counter()
+ .count(), 0);
+ manager.close();
+ }
+
+ @Test
+ public void hasResourceReturnsFalse() {
+ manager = new
ResourceThrottlerManager(mockResourceThrottler.getClass().getName(),
pluginManager);
+ when(mockResourceThrottler.hasResource(anyString(),
any(TenantResourceType.class)))
+ .thenReturn(false);
+ boolean hasResource = manager.hasResource(tenantId, resourceType);
+ assertFalse(hasResource);
+ assertEquals(meterRegistry.find("call.exec.timer")
+ .tag("method", "ResourceThrottler/hasResource")
+ .timer()
+ .count(), 1);
+ assertEquals(meterRegistry.find("call.exec.fail.count")
+ .tag("method", "ResourceThrottler/hasResource")
+ .counter()
+ .count(), 0);
+ manager.close();
+ }
+
+ @Test
+ public void hasResourceThrowsException() {
+ manager = new
ResourceThrottlerManager(mockResourceThrottler.getClass().getName(),
pluginManager);
+ when(mockResourceThrottler.hasResource(anyString(),
any(TenantResourceType.class)))
+ .thenThrow(new RuntimeException("Intend Error"));
+ boolean hasResource = manager.hasResource(tenantId, resourceType);
+ // Should return true when exception occurs (fail-safe)
+ assertTrue(hasResource);
+ assertEquals(meterRegistry.find("call.exec.timer")
+ .tag("method", "ResourceThrottler/hasResource")
+ .timer()
+ .count(), 0);
+ assertEquals(meterRegistry.find("call.exec.fail.count")
+ .tag("method", "ResourceThrottler/hasResource")
+ .counter()
+ .count(), 1);
+ manager.close();
+ }
+
+ @Test
+ public void close() {
+ manager = new
ResourceThrottlerManager(mockResourceThrottler.getClass().getName(),
pluginManager);
+ manager.close();
+ // Should be idempotent
+ manager.close();
+ }
+
+ @Test
+ public void testAllResourceTypes() {
+
when(pluginManager.getExtensions(IResourceThrottler.class)).thenReturn(Collections.emptyList());
+ manager = new ResourceThrottlerManager(null, pluginManager);
+ // Test all resource types in dev only mode
+ for (TenantResourceType type : TenantResourceType.values()) {
+ assertTrue(manager.hasResource(tenantId, type));
+ }
+ manager.close();
+ }
+
+ static class FirstTestResourceThrottler implements IResourceThrottler {
+ @Override
+ public boolean hasResource(String tenantId, TenantResourceType type) {
+ return true;
+ }
+ }
+
+ static class SecondTestResourceThrottler implements IResourceThrottler {
+ @Override
+ public boolean hasResource(String tenantId, TenantResourceType type) {
+ return false;
+ }
+ }
+}
diff --git
a/bifromq-plugin/bifromq-plugin-resource-throttler-helper/src/test/java/org/apache/bifromq/plugin/resourcethrottler/TenantResourceThrottlerManagerTest.java
b/bifromq-plugin/bifromq-plugin-resource-throttler-helper/src/test/java/org/apache/bifromq/plugin/resourcethrottler/TenantResourceThrottlerManagerTest.java
index 72c175587..b7aa2b5ad 100644
---
a/bifromq-plugin/bifromq-plugin-resource-throttler-helper/src/test/java/org/apache/bifromq/plugin/resourcethrottler/TenantResourceThrottlerManagerTest.java
+++
b/bifromq-plugin/bifromq-plugin-resource-throttler-helper/src/test/java/org/apache/bifromq/plugin/resourcethrottler/TenantResourceThrottlerManagerTest.java
@@ -65,13 +65,10 @@ public class TenantResourceThrottlerManagerTest {
manager.close();
}
- @Test
+ @Test(expectedExceptions = ResourceThrottlerException.class)
public void pluginNotFound() {
ResourceThrottlerManager devOnlyManager = new
ResourceThrottlerManager(null, pluginManager);
manager = new ResourceThrottlerManager("Fake", pluginManager);
- for (TenantResourceType type : TenantResourceType.values()) {
- assertEquals(devOnlyManager.hasResource(tenantId, type),
manager.hasResource(tenantId, type));
- }
devOnlyManager.close();
}
diff --git
a/bifromq-plugin/bifromq-plugin-setting-provider-helper/src/main/java/org/apache/bifromq/plugin/settingprovider/SettingProviderException.java
b/bifromq-plugin/bifromq-plugin-setting-provider-helper/src/main/java/org/apache/bifromq/plugin/settingprovider/SettingProviderException.java
new file mode 100644
index 000000000..9496b299d
--- /dev/null
+++
b/bifromq-plugin/bifromq-plugin-setting-provider-helper/src/main/java/org/apache/bifromq/plugin/settingprovider/SettingProviderException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.plugin.settingprovider;
+
+import org.pf4j.util.StringUtils;
+
+public class SettingProviderException extends RuntimeException {
+
+ public SettingProviderException(String message, Object... args) {
+ super(StringUtils.format(message, args));
+ }
+
+}
diff --git
a/bifromq-plugin/bifromq-plugin-setting-provider-helper/src/main/java/org/apache/bifromq/plugin/settingprovider/SettingProviderManager.java
b/bifromq-plugin/bifromq-plugin-setting-provider-helper/src/main/java/org/apache/bifromq/plugin/settingprovider/SettingProviderManager.java
index 7ecb8bb1b..b1e2f1e48 100644
---
a/bifromq-plugin/bifromq-plugin-setting-provider-helper/src/main/java/org/apache/bifromq/plugin/settingprovider/SettingProviderManager.java
+++
b/bifromq-plugin/bifromq-plugin-setting-provider-helper/src/main/java/org/apache/bifromq/plugin/settingprovider/SettingProviderManager.java
@@ -20,6 +20,7 @@
package org.apache.bifromq.plugin.settingprovider;
import java.util.Map;
+import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@@ -35,18 +36,28 @@ public class SettingProviderManager implements
ISettingProvider, AutoCloseable {
public SettingProviderManager(String settingProviderFQN, PluginManager
pluginMgr) {
Map<String, ISettingProvider> availSettingProviders =
pluginMgr.getExtensions(ISettingProvider.class).stream()
- .collect(Collectors.toMap(e -> e.getClass().getName(), e -> e));
+ .collect(Collectors.toMap(e -> e.getClass().getName(), e -> e,
+ (k,v) -> v, TreeMap::new));
if (availSettingProviders.isEmpty()) {
pluginLog.warn("No setting provider plugin available, use DEV ONLY
one instead");
+
+
provider = new MonitoredSettingProvider(new
DevOnlySettingProvider());
} else {
if (settingProviderFQN == null) {
- pluginLog.warn("Setting provider plugin type not specified,
use DEV ONLY one instead");
- provider = new MonitoredSettingProvider(new
DevOnlySettingProvider());
+ if (availSettingProviders.size() > 1) {
+ pluginLog.info("Setting provider plugin type not
specified, use the first found");
+ }
+ String firstSettingProviderFQN =
availSettingProviders.keySet().iterator().next();
+ pluginLog.info("Setting provider plugin loaded: {}",
firstSettingProviderFQN);
+ provider = new CacheableSettingProvider(
+ new
MonitoredSettingProvider(availSettingProviders.get(firstSettingProviderFQN)),
+ CacheOptions.DEFAULT);
} else if (!availSettingProviders.containsKey(settingProviderFQN))
{
- pluginLog.warn("Setting provider plugin type '{}' not found,
use DEV ONLY one instead",
+ pluginLog.warn("Setting provider plugin type '{}' not found,
so the system will shut down.",
settingProviderFQN);
- provider = new MonitoredSettingProvider(new
DevOnlySettingProvider());
+ throw new SettingProviderException("Setting provider plugin
type '%s' not found, so the system will shut down.",
+ settingProviderFQN);
} else {
pluginLog.info("Setting provider plugin type: {}",
settingProviderFQN);
provider = new CacheableSettingProvider(
diff --git
a/bifromq-plugin/bifromq-plugin-setting-provider-helper/src/test/java/org/apache/bifromq/plugin/settingprovider/SettingProviderManagerTest.java
b/bifromq-plugin/bifromq-plugin-setting-provider-helper/src/test/java/org/apache/bifromq/plugin/settingprovider/SettingProviderManagerTest.java
index 6807f05d1..32c03cf5e 100644
---
a/bifromq-plugin/bifromq-plugin-setting-provider-helper/src/test/java/org/apache/bifromq/plugin/settingprovider/SettingProviderManagerTest.java
+++
b/bifromq-plugin/bifromq-plugin-setting-provider-helper/src/test/java/org/apache/bifromq/plugin/settingprovider/SettingProviderManagerTest.java
@@ -14,68 +14,132 @@
* "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.plugin.settingprovider;
import static org.awaitility.Awaitility.await;
+import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
-import org.pf4j.DefaultPluginManager;
+import java.util.Arrays;
+import java.util.Collections;
+import lombok.extern.slf4j.Slf4j;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
import org.pf4j.PluginManager;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+@Slf4j
public class SettingProviderManagerTest {
- private final String tenantId = "tenantA";
- private SettingProviderManager manager;
+ private static final String TENANT_ID = "tenantA";
+ @Mock
private PluginManager pluginManager;
+ @Mock
+ private ISettingProvider mockProvider;
+ private SettingProviderManager manager;
+ private AutoCloseable closeable;
@BeforeMethod
public void setup() {
// to speed up tests
System.setProperty(CacheOptions.SettingCacheOptions.SYS_PROP_SETTING_REFRESH_SECONDS,
"1");
- pluginManager = new DefaultPluginManager();
- pluginManager.loadPlugins();
- pluginManager.startPlugins();
+ closeable = MockitoAnnotations.openMocks(this);
+ when(pluginManager.getExtensions(ISettingProvider.class)).thenReturn(
+ Collections.singletonList(mockProvider));
}
@AfterMethod
- public void teardown() {
- pluginManager.stopPlugins();
- pluginManager.unloadPlugins();
+ public void tearDown() throws Exception {
+ if (manager != null) {
+ manager.close();
+ }
+ closeable.close();
+
System.clearProperty(CacheOptions.SettingCacheOptions.SYS_PROP_SETTING_REFRESH_SECONDS);
}
@Test
public void devOnlyMode() {
+
when(pluginManager.getExtensions(ISettingProvider.class)).thenReturn(Collections.emptyList());
manager = new SettingProviderManager(null, pluginManager);
- manager.provide(Setting.DebugModeEnabled, tenantId);
- manager.close();
+ for (Setting setting : Setting.values()) {
+ assertEquals(manager.provide(setting, TENANT_ID), (Object)
setting.initialValue());
+ }
}
@Test
public void pluginSpecified() {
- manager = new
SettingProviderManager(SettingProviderTestStub.class.getName(), pluginManager);
- await().until(() -> (int) manager.provide(Setting.MaxTopicLevels,
tenantId) == 64);
- manager.close();
+ when(mockProvider.provide(Setting.MaxTopicLevels,
TENANT_ID)).thenReturn(64);
+ manager = new
SettingProviderManager(mockProvider.getClass().getName(), pluginManager);
+ await().until(() -> (int) manager.provide(Setting.MaxTopicLevels,
TENANT_ID) == 64);
+ }
+
+ @Test
+ public void pluginNotSpecifiedWithSingleProvider() {
+ when(mockProvider.provide(Setting.MaxTopicLevels,
TENANT_ID)).thenReturn(32);
+ manager = new SettingProviderManager(null, pluginManager);
+ await().until(() -> (int) manager.provide(Setting.MaxTopicLevels,
TENANT_ID) == 32);
}
@Test
+ public void pluginNotSpecifiedWithMultipleProviders() {
+ ISettingProvider provider1 = new FirstTestSettingProvider();
+ ISettingProvider provider2 = new SecondTestSettingProvider();
+
+ when(pluginManager.getExtensions(ISettingProvider.class)).thenReturn(
+ Arrays.asList(provider1, provider2));
+ manager = new SettingProviderManager(null, pluginManager);
+
+ await().until(() -> (int) manager.provide(Setting.MaxTopicLevels,
TENANT_ID) == 100);
+ }
+
+ @Test
+ public void pluginNotSpecifiedWithMultipleSortByKeyProviders() {
+ ISettingProvider provider1 = new FirstTestSettingProvider();
+ ISettingProvider provider2 = new SecondTestSettingProvider();
+
+ when(pluginManager.getExtensions(ISettingProvider.class)).thenReturn(
+ Arrays.asList(provider2, provider1));
+ manager = new SettingProviderManager(null, pluginManager);
+
+ await().until(() -> (int) manager.provide(Setting.MaxTopicLevels,
TENANT_ID) == 100);
+ }
+
+ @Test(expectedExceptions = SettingProviderException.class)
public void pluginNotFound() {
- SettingProviderManager devOnlyManager = new
SettingProviderManager(null, pluginManager);
manager = new SettingProviderManager("Fake", pluginManager);
- for (Setting setting : Setting.values()) {
- assertEquals(devOnlyManager.provide(setting, tenantId),
- (Object) manager.provide(setting, tenantId));
- }
- devOnlyManager.close();
+ manager.close();
}
@Test
public void stop() {
- manager = new
SettingProviderManager(SettingProviderTestStub.class.getName(), pluginManager);
+ manager = new
SettingProviderManager(mockProvider.getClass().getName(), pluginManager);
manager.close();
+ manager = null;
+ }
+
+ static class FirstTestSettingProvider implements ISettingProvider {
+ @SuppressWarnings("unchecked")
+ @Override
+ public <R> R provide(Setting setting, String tenantId) {
+ if (setting == Setting.MaxTopicLevels) {
+ return (R) Integer.valueOf(100);
+ }
+ return setting.initialValue();
+ }
+ }
+
+ static class SecondTestSettingProvider implements ISettingProvider {
+ @SuppressWarnings("unchecked")
+ @Override
+ public <R> R provide(Setting setting, String tenantId) {
+ if (setting == Setting.MaxTopicLevels) {
+ return (R) Integer.valueOf(200);
+ }
+ return setting.initialValue();
+ }
}
}