Browse Source
Introduces a per-day quota derived from the monthly limit using a configurable peak-days multiplier (default 3): dailyThreshold = monthlyThreshold * peakDays * DAY_MS / monthDurationMs A tenant with a 10M/month transport quota is allowed up to ~1M messages per day (3× daily fair-share). Exceeding the daily limit disables the feature for that day; it auto-recovers at the next day boundary without affecting the monthly cumulative counter. Changes: - SchedulerUtils: add getStartOfCurrentDay() - DefaultTenantProfileConfiguration: add dailyPeakDays field (default 3) - BaseApiUsageState: add currentDayValues map, currentDayTs tracking, setDay() reset, setDaily/getDaily accessors, daily fields in StatsCalculationResult - TenantApiUsageState: add getDailyThreshold / getDailyWarnThreshold, enforce both monthly and daily thresholds in checkStateUpdatedDueToThreshold - DefaultTbApiUsageStateService: detect day rollover, write Daily timeseries entries, restore daily values from timeseries on startup - TenantApiUsageStateDailyTest: 6 unit tests covering threshold formula, unlimited passthrough, warn fraction, and peakDays configurability Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>pull/15148/head
6 changed files with 221 additions and 5 deletions
@ -0,0 +1,136 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard Authors |
|||
* |
|||
* Licensed 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.thingsboard.server.service.apiusage; |
|||
|
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.thingsboard.server.common.data.ApiUsageRecordKey; |
|||
import org.thingsboard.server.common.data.ApiUsageState; |
|||
import org.thingsboard.server.common.data.TenantProfile; |
|||
import org.thingsboard.server.common.data.id.ApiUsageStateId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; |
|||
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; |
|||
import org.thingsboard.server.common.msg.tools.SchedulerUtils; |
|||
|
|||
import java.util.UUID; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.assertj.core.api.Assertions.within; |
|||
|
|||
public class TenantApiUsageStateDailyTest { |
|||
|
|||
private static final long MONTHLY = 10_000_000L; |
|||
private static final double WARN_FRACTION = 0.8; |
|||
private static final int PEAK_DAYS = 3; |
|||
|
|||
private TenantProfile tenantProfile; |
|||
private long currentCycleTs; |
|||
private long nextCycleTs; |
|||
|
|||
@Before |
|||
public void setUp() { |
|||
currentCycleTs = SchedulerUtils.getStartOfCurrentMonth(); |
|||
nextCycleTs = SchedulerUtils.getStartOfNextMonth(); |
|||
|
|||
DefaultTenantProfileConfiguration config = DefaultTenantProfileConfiguration.builder() |
|||
.maxTransportMessages(MONTHLY) |
|||
.warnThreshold(WARN_FRACTION) |
|||
.dailyPeakDays(PEAK_DAYS) |
|||
.build(); |
|||
TenantProfileData profileData = new TenantProfileData(); |
|||
profileData.setConfiguration(config); |
|||
tenantProfile = new TenantProfile(); |
|||
tenantProfile.setProfileData(profileData); |
|||
} |
|||
|
|||
private TenantApiUsageState createState() { |
|||
ApiUsageState apiUsageState = new ApiUsageState(new ApiUsageStateId(UUID.randomUUID())); |
|||
apiUsageState.setTenantId(TenantId.fromUUID(UUID.randomUUID())); |
|||
apiUsageState.setEntityId(TenantId.fromUUID(UUID.randomUUID())); |
|||
return new TenantApiUsageState(tenantProfile, apiUsageState); |
|||
} |
|||
|
|||
@Test |
|||
public void testDailyThresholdIsProportionalToMonthly() { |
|||
TenantApiUsageState state = createState(); |
|||
|
|||
long daily = state.getDailyThreshold(ApiUsageRecordKey.TRANSPORT_MSG_COUNT); |
|||
long monthMs = nextCycleTs - currentCycleTs; |
|||
long expected = MONTHLY * PEAK_DAYS * TimeUnit.DAYS.toMillis(1) / monthMs; |
|||
|
|||
assertThat(daily).isEqualTo(expected); |
|||
// Roughly 3/30 of monthly for a 30-day month
|
|||
double ratio = (double) daily / MONTHLY; |
|||
assertThat(ratio).isCloseTo((double) PEAK_DAYS / 30, within(0.01)); |
|||
} |
|||
|
|||
@Test |
|||
public void testDailyThresholdIsLessThanMonthly() { |
|||
TenantApiUsageState state = createState(); |
|||
assertThat(state.getDailyThreshold(ApiUsageRecordKey.TRANSPORT_MSG_COUNT)) |
|||
.isLessThan(MONTHLY); |
|||
} |
|||
|
|||
@Test |
|||
public void testUnlimitedMonthlyGivesUnlimitedDaily() { |
|||
DefaultTenantProfileConfiguration config = DefaultTenantProfileConfiguration.builder() |
|||
.maxTransportMessages(0L) |
|||
.build(); |
|||
TenantProfileData profileData = new TenantProfileData(); |
|||
profileData.setConfiguration(config); |
|||
tenantProfile.setProfileData(profileData); |
|||
|
|||
TenantApiUsageState state = createState(); |
|||
assertThat(state.getDailyThreshold(ApiUsageRecordKey.TRANSPORT_MSG_COUNT)).isZero(); |
|||
} |
|||
|
|||
@Test |
|||
public void testDailyWarnThresholdAppliesWarnFraction() { |
|||
TenantApiUsageState state = createState(); |
|||
|
|||
long daily = state.getDailyThreshold(ApiUsageRecordKey.TRANSPORT_MSG_COUNT); |
|||
long dailyWarn = state.getDailyWarnThreshold(ApiUsageRecordKey.TRANSPORT_MSG_COUNT); |
|||
|
|||
assertThat((double) dailyWarn / daily).isCloseTo(WARN_FRACTION, within(0.001)); |
|||
} |
|||
|
|||
@Test |
|||
public void testMonthlyThresholdUnchanged() { |
|||
TenantApiUsageState state = createState(); |
|||
assertThat(state.getProfileThreshold(ApiUsageRecordKey.TRANSPORT_MSG_COUNT)).isEqualTo(MONTHLY); |
|||
} |
|||
|
|||
@Test |
|||
public void testDailyThresholdRespectsPeakDaysConfig() { |
|||
// peakDays=1 → daily ≈ 1/30 monthly; peakDays=6 → daily ≈ 6/30 monthly
|
|||
for (int days : new int[]{1, 3, 6}) { |
|||
DefaultTenantProfileConfiguration config = DefaultTenantProfileConfiguration.builder() |
|||
.maxTransportMessages(MONTHLY) |
|||
.warnThreshold(WARN_FRACTION) |
|||
.dailyPeakDays(days) |
|||
.build(); |
|||
TenantProfileData profileData = new TenantProfileData(); |
|||
profileData.setConfiguration(config); |
|||
tenantProfile.setProfileData(profileData); |
|||
|
|||
TenantApiUsageState state = createState(); |
|||
double ratio = (double) state.getDailyThreshold(ApiUsageRecordKey.TRANSPORT_MSG_COUNT) / MONTHLY; |
|||
assertThat(ratio).isCloseTo((double) days / 30, within(0.01)); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue