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.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 AuthProviderManager(String authProviderFQN, this.settingProvider = settingProvider; this.eventCollector = eventCollector; Map 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.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 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 void pluginSpecified() { } @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 void byPassCheckResultError() { assertEquals(meterRegistry.find(CALL_FAIL_COUNTER).tag(TAG_METHOD, "AuthProvider/check").counter().count(), 0); } + + static class FirstTestAuthProvider implements IAuthProvider { + @Override + public CompletableFuture auth(MQTT3AuthData authData) { + return CompletableFuture.completedFuture(MQTT3AuthResult.newBuilder() + .setOk(Ok.newBuilder().setTenantId("FirstProvider").build()).build()); + } + + @Override + public CompletableFuture check(ClientInfo client, MQTTAction action) { + return CompletableFuture.completedFuture(true); + } + } + + static class SecondTestAuthProvider implements IAuthProvider { + @Override + public CompletableFuture auth(MQTT3AuthData authData) { + return CompletableFuture.completedFuture(MQTT3AuthResult.newBuilder() + .setOk(Ok.newBuilder().setTenantId("SecondProvider").build()).build()); + } + + @Override + public CompletableFuture 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.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 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 void pluginSpecified() { 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 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 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 provide(Setting setting, String tenantId) { + if (setting == Setting.MaxTopicLevels) { + return (R) Integer.valueOf(200); + } + return setting.initialValue(); + } } }