001/* 002 * Copyright 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet; 018 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.NotThreadSafe; 023import javax.annotation.concurrent.ThreadSafe; 024import java.time.Duration; 025 026import static java.util.Objects.requireNonNull; 027 028/** 029 * Immutable finite configuration for Soklet's built-in in-memory token-bucket 030 * rate limiter. Capacity, refill quantity, and refill interval must all be 031 * explicitly configured. This type has no unlimited or disabled form. 032 * 033 * @author <a href="https://www.revetkn.com">Mark Allen</a> 034 */ 035@ThreadSafe 036public final class McpTokenBucketConfig { 037 private final long capacity; 038 private final long refillTokens; 039 @NonNull 040 private final Duration refillInterval; 041 042 /** 043 * Vends a builder primed with the bucket's maximum token capacity. 044 * 045 * @param capacity positive maximum number of tokens 046 * @return token-bucket configuration builder 047 * @throws NullPointerException if {@code capacity} is null 048 */ 049 @NonNull 050 public static Builder withCapacity(@NonNull Long capacity) { 051 return new Builder().capacity(capacity); 052 } 053 054 private McpTokenBucketConfig(@NonNull Builder builder) { 055 this.capacity = requirePositive(builder.capacity, "capacity"); 056 if (builder.refillTokens == null || builder.refillInterval == null) 057 throw new IllegalStateException( 058 "refillTokens and refillInterval must be explicitly configured"); 059 this.refillTokens = requirePositive(builder.refillTokens, "refillTokens"); 060 this.refillInterval = requireNonNull(builder.refillInterval, 061 "refillInterval"); 062 063 if (this.refillInterval.isZero() || this.refillInterval.isNegative()) 064 throw new IllegalArgumentException("refillInterval must be positive"); 065 try { 066 if (this.refillInterval.toNanos() <= 0) 067 throw new IllegalArgumentException( 068 "refillInterval must be at least one nanosecond"); 069 } catch (ArithmeticException exception) { 070 throw new IllegalArgumentException( 071 "refillInterval must fit in a signed 64-bit nanosecond count", 072 exception); 073 } 074 } 075 076 /** 077 * Returns the maximum number of tokens held by one bucket. 078 * 079 * @return positive token capacity 080 */ 081 @NonNull 082 public Long getCapacity() { 083 return this.capacity; 084 } 085 086 /** 087 * Returns the number of tokens replenished during one refill interval. 088 * 089 * @return positive refill quantity 090 */ 091 @NonNull 092 public Long getRefillTokens() { 093 return this.refillTokens; 094 } 095 096 /** 097 * Returns the refill interval. 098 * 099 * @return positive refill interval 100 */ 101 @NonNull 102 public Duration getRefillInterval() { 103 return this.refillInterval; 104 } 105 106 /** @return whether every token-bucket setting is equal */ 107 @Override 108 public boolean equals(@Nullable Object other) { 109 return this == other 110 || other instanceof McpTokenBucketConfig configuration 111 && this.capacity == configuration.capacity 112 && this.refillTokens == configuration.refillTokens 113 && this.refillInterval.equals(configuration.refillInterval); 114 } 115 116 /** @return structural token-bucket configuration hash code */ 117 @Override 118 public int hashCode() { 119 int result = Long.hashCode(this.capacity); 120 result = 31 * result + Long.hashCode(this.refillTokens); 121 result = 31 * result + this.refillInterval.hashCode(); 122 return result; 123 } 124 125 private static long requirePositive(long value, @NonNull String name) { 126 if (value <= 0) 127 throw new IllegalArgumentException(name + " must be positive"); 128 return value; 129 } 130 131 /** 132 * Single-threaded builder for immutable token-bucket configurations. 133 * 134 * @author <a href="https://www.revetkn.com">Mark Allen</a> 135 */ 136 @NotThreadSafe 137 public static final class Builder { 138 private long capacity; 139 @Nullable 140 private Long refillTokens; 141 @Nullable 142 private Duration refillInterval; 143 144 private Builder() {} 145 146 /** 147 * Sets the maximum number of tokens held by one bucket. 148 * 149 * @param capacity positive token capacity 150 * @return this builder 151 * @throws NullPointerException if {@code capacity} is null 152 */ 153 @NonNull 154 public Builder capacity(@NonNull Long capacity) { 155 this.capacity = requireNonNull(capacity); 156 return this; 157 } 158 159 /** 160 * Sets the number of tokens replenished during one refill interval. 161 * This value is required and has no default. 162 * 163 * @param refillTokens positive refill quantity 164 * @return this builder 165 * @throws NullPointerException if {@code refillTokens} is null 166 */ 167 @NonNull 168 public Builder refillTokens(@NonNull Long refillTokens) { 169 this.refillTokens = requireNonNull(refillTokens); 170 return this; 171 } 172 173 /** 174 * Sets the positive refill interval. This value is required and has no 175 * default. 176 * 177 * @param refillInterval positive refill interval 178 * @return this builder 179 * @throws NullPointerException if {@code refillInterval} is null 180 */ 181 @NonNull 182 public Builder refillInterval(@NonNull Duration refillInterval) { 183 this.refillInterval = requireNonNull(refillInterval); 184 return this; 185 } 186 187 /** 188 * Builds an immutable finite configuration. 189 * 190 * @return token-bucket configuration 191 * @throws IllegalStateException if refill quantity or interval is absent 192 * @throws IllegalArgumentException if a numeric value is not positive or 193 * the refill interval cannot be represented 194 * in nanoseconds 195 */ 196 @NonNull 197 public McpTokenBucketConfig build() { 198 return new McpTokenBucketConfig(this); 199 } 200 } 201}