Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rate-limit #114

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ codegen {

dependencies {
api(VertxLibs.core)
api(project(":ratelimit:api"))
compileOnly(JacksonLibs.annotations)
compileOnly(JacksonLibs.databind)
compileOnly(UtilLibs.jetbrainsAnnotations)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import io.github.zero88.schedulerx.trigger.TriggerCondition.ReasonCode;
import io.github.zero88.schedulerx.trigger.TriggerContext;
import io.github.zero88.schedulerx.trigger.TriggerEvaluator;
import io.github.zero88.schedulerx.trigger.policy.TriggerRateLimitConfig;
import io.github.zero88.schedulerx.trigger.policy.TriggerRateLimitRepository;
import io.github.zero88.schedulerx.trigger.rule.TriggerRule;
import io.vertx.core.Future;
import io.vertx.core.Promise;
Expand Down Expand Up @@ -69,6 +71,8 @@ public abstract class AbstractScheduler<IN, OUT, T extends Trigger>
private boolean didStart = false;
private boolean didTriggerValidation = false;
private IllegalArgumentException invalidTrigger;
private Object policyKey;
private TriggerRateLimitRepository repository;

protected AbstractScheduler(Vertx vertx, TimeClock clock, SchedulingMonitor<OUT> monitor, Job<IN, OUT> job,
JobData<IN> jobData, TimeoutPolicy timeoutPolicy, T trigger,
Expand Down Expand Up @@ -108,14 +112,23 @@ protected AbstractScheduler(Vertx vertx, TimeClock clock, SchedulingMonitor<OUT>
@Override
@SuppressWarnings({ "java:S1193", "unchecked" })
public final @NotNull T trigger() {
if (didTriggerValidation) {
if (invalidTrigger == null) { return trigger; }
throw invalidTrigger;
}
lock.lock();
try {
if (didTriggerValidation) {
if (invalidTrigger == null) { return trigger; }
throw invalidTrigger;
}
try {
return (T) this.trigger.validate();
final T theTrigger = (T) this.trigger.validate();
final TriggerRateLimitConfig rateLimitConfig = theTrigger.ratelimitConfig();
if (rateLimitConfig != null) {
if (rateLimitConfig.keyGenerator().requireExternalKey() && jobData().externalId() == null) {
throw new IllegalArgumentException(
"The external id is required to make rate-limit policy works properly");
}
this.policyKey = rateLimitConfig.keyGenerator().generate(theTrigger, jobData().externalId());
}
return theTrigger;
} catch (Exception ex) {
if (ex instanceof IllegalArgumentException) {
this.invalidTrigger = (IllegalArgumentException) ex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.jetbrains.annotations.Nullable;

import io.github.zero88.schedulerx.Job;
import io.github.zero88.schedulerx.trigger.policy.TriggerRateLimitConfig;
import io.github.zero88.schedulerx.trigger.repr.TriggerRepresentation;
import io.github.zero88.schedulerx.trigger.repr.TriggerRepresentationServiceLoader;
import io.github.zero88.schedulerx.trigger.rule.TriggerRule;
Expand Down Expand Up @@ -35,6 +36,18 @@ public interface Trigger extends HasTriggerType, TriggerRepresentation {
return TriggerRule.NOOP;
}

/**
* Defines the rate-limit configuration
*
* @return the rate-limit configuration or {@code null} means restrict one execution per trigger that can be run
* in a certain time
* @see TriggerRateLimitConfig
* @since 2.0.0
*/
default @Nullable TriggerRateLimitConfig ratelimitConfig() {
return null;
}

/**
* Do validate trigger in runtime.
*
Expand Down Expand Up @@ -73,6 +86,7 @@ public interface Trigger extends HasTriggerType, TriggerRepresentation {
default JsonObject toJson() {
JsonObject json = JsonObject.of("type", type());
if (rule() != TriggerRule.NOOP) { json.put("rule", rule()); }
if (ratelimitConfig() != null) { json.put("ratelimit", ratelimitConfig().toJson()); }
return json;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class ReasonCode {
public static final String STOP_BY_JOB = "ForceStop";
public static final String STOP_BY_CONFIG = "StopByTriggerConfig";
public static final String JOB_IS_RUNNING = "JobIsRunning";
public static final String RATE_LIMIT = "RateLimit";
public static final String UNEXPECTED_ERROR = "UnexpectedError";

private ReasonCode() { }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.github.zero88.schedulerx.trigger.policy;

import org.jetbrains.annotations.NotNull;

import io.github.zero88.ratelimit.RateLimitConfig;

/**
* A rate-limit mechanism to control the number of execution rounds of one trigger can be run in parallel in a certain
* period.
*
* @since 2.0.0
*/
public interface TriggerRateLimitConfig extends RateLimitConfig {

@NotNull TriggerRateLimitKeyGenerator keyGenerator();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.github.zero88.schedulerx.trigger.policy;

public interface TriggerRateLimitHelper {

boolean isUnlimited();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.github.zero88.schedulerx.trigger.policy;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import io.github.zero88.ratelimit.RateLimitKeyGenerator;
import io.github.zero88.schedulerx.JobData;
import io.github.zero88.schedulerx.trigger.Trigger;

public interface TriggerRateLimitKeyGenerator extends RateLimitKeyGenerator {

boolean requireExternalKey();

/**
* Generate rate-limit policy key for the trigger
*
* @param trigger the trigger.
* @param externalKey the external trigger key. See {@link JobData#externalId()}
* @return the unique key in the rate-limit system
* @apiNote to identify a trigger to apply rate-limiting, the concrete key generator can combine some properties
* such as {@code externalKey}, {@code triggerType} and/or {@code triggerContextInfo}.
* @see Trigger
*/
@NotNull Object generate(@NotNull Trigger trigger, @Nullable Object externalKey);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.github.zero88.schedulerx.trigger.policy;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import io.github.zero88.ratelimit.RateLimitPolicyRepository;
import io.github.zero88.ratelimit.RateLimitResult;
import io.github.zero88.schedulerx.trigger.TriggerContext;
import io.vertx.core.Future;

public interface TriggerRateLimitRepository extends RateLimitPolicyRepository<TriggerContext> {

@NotNull Future<RateLimitResult<TriggerContext>> increase(@NotNull Object policyKey,
@Nullable TriggerContext eventContext);

}
2 changes: 2 additions & 0 deletions ratelimit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Rate-limit

10 changes: 10 additions & 0 deletions ratelimit/api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
oss {
title.set("The rate-limit API")
}

dependencies {
api(VertxLibs.core)
compileOnly(JacksonLibs.annotations)
compileOnly(JacksonLibs.databind)
compileOnly(UtilLibs.jetbrainsAnnotations)
}
3 changes: 3 additions & 0 deletions ratelimit/api/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Project properties --------------------------
title=The rate-limit API
description=The rate-limit API
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.github.zero88.ratelimit;

/**
* Represents for a thing which links two or more thing together
*/
public interface Connector {

ConnectorConfig config();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.github.zero88.ratelimit;

public interface ConnectorConfig { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.github.zero88.ratelimit;

import java.time.Duration;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* Represents for the rate-limit configuration that enables you to manage the event traffic for your specific purpose,
* such as limit the access to REST APIs, throttle event-bus messages on specific address.
* <p/>
* When the first event is going to start. This event fixes the time window. Each event consumes quota from the
* current window until the time expires. When quota is exhausted, the resulting action depends on the policy:
* <ul>
* <li>Rate limiting rejects the event.</li>
* <li>Throttling queues the event for retry.</li>
* </ul>
* When the time window closes, quota is reset and a new window of the same fixed size starts.
*
* @since 1.0.0
*/
public interface LimitConfig {

/**
* @return The maximum number of event that can be run in parallel during the sliding time window in
* {@link #timeWindow()}
*/
int limit();

/**
* @return the sliding time window during which the number of allowed events should not exceed the value
* specified in {@link #limit()}
*/
@NotNull Duration timeWindow();

/**
* @return The throttle configuration
* @see ThrottleConfig
*/
@Nullable ThrottleConfig throttleConfig();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.github.zero88.ratelimit;

/**
* @since 1.0.0
*/
public enum RateLimitAction {
/**
* Accept to process new event
*/
ACCEPTED,
/**
* Reject to process new event
*/
REJECTED,
/**
* Move the new event into queue for retry later
*/
THROTTLING
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.github.zero88.ratelimit;

import java.time.Duration;
import java.util.List;

import org.jetbrains.annotations.NotNull;

import io.vertx.core.json.JsonObject;

/**
* A rate-limit mechanism to control the maximum number of events can be processed in parallel in a certain period.
*
* @since 1.0.0
*/
public interface RateLimitConfig {

/**
* Declares the set of limit configuration
*
* @return the limit configuration
* @see LimitConfig
*/
@NotNull List<LimitConfig> limits();

/**
* Declares a request timeout when increasing the rate-limit counter
*
* @return the timeout
*/
@NotNull Duration timeout();

/**
* Declares the generator to generate the rate-limit policy key.
*
* @return the key generator
* @see RateLimitKeyGenerator
*/
@NotNull RateLimitKeyGenerator keyGenerator();

@NotNull JsonObject toJson();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.zero88.ratelimit;

public class RateLimitException extends RuntimeException {

private final RateLimitResult<Object> result;

public RateLimitException(RateLimitResult<Object> result) { this.result = result; }

public RateLimitResult<Object> getResult() {
return result;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.github.zero88.ratelimit;

/**
* Represents for a generator to generate a rate-limit policy key.
* <p/>
* The rate-limit policy key is a unique id of the thing for which to apply the rate-limit in one rate-limit policy
* repository,
*
* @since 1.0.0
* @see RateLimitPolicyRepository
*/
public interface RateLimitKeyGenerator {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.github.zero88.ratelimit;

import org.jetbrains.annotations.NotNull;

import io.vertx.core.Future;
import io.vertx.core.Vertx;

/**
* A rate-limit mechanism to control the maximum number of events can be processed in parallel in a certain period.
*
* @since 1.0.0
*/
public interface RateLimitPolicy {

@NotNull Object policeKey();

@NotNull RateLimitConfig config();

/**
* Set up the rate-limit policy
*
* @param vertx Vertx
* @return a reference to this for fluent API
*/
@NotNull Future<RateLimitPolicy> initialize(@NotNull Vertx vertx);

Future<Void> destroy();

/**
* Increase the event counter before new event is processed by the system
*
* @return a rate-limit result
* @see RateLimitResult
*/
@NotNull <T> Future<RateLimitResult<T>> increase(T context);

/**
* Decrease the event counter when the event is finished
*/
void decrease();

}
Loading
Loading