Skip to content

airframe-control: Add RateLimiter with token bucket algorithm#3942

Merged
xerial merged 6 commits intomainfrom
copilot/fix-1027-2
May 31, 2025
Merged

airframe-control: Add RateLimiter with token bucket algorithm#3942
xerial merged 6 commits intomainfrom
copilot/fix-1027-2

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 31, 2025

This PR implements a RateLimiter for airframe-control using a token bucket algorithm to control the rate of operations. This addresses the need for rate limiting and retry budget control mentioned in the issue.

Features

Core Functionality

  • Token bucket algorithm with configurable permits per second and burst size
  • Thread-safe implementation using atomic operations with immutable state
  • Cross-platform compatibility (JVM, JS, Native) using Compat.sleep
  • Blocking and non-blocking operations: acquire() and tryAcquire() methods
  • Timeout support: tryAcquire(permits, timeout, unit)

Usage Examples

Basic rate limiting:

import wvlet.airframe.control.RateLimiter

// Allow 10 permits per second
val limiter = RateLimiter.create(10.0)

// Acquire a permit (blocks if necessary)
limiter.acquire()

// Try to acquire without blocking
if (limiter.tryAcquire()) {
  // Permit acquired
}

Burst control:

// Allow 5 permits/sec with burst of up to 20 permits
val limiter = RateLimiter.create(5.0, 20)

// Can immediately acquire up to 20 permits
for (_ <- 1 to 20) {
  limiter.tryAcquire() // All succeed immediately
}

Per-host rate limiting:

import scala.collection.concurrent.TrieMap

val hostLimiters = TrieMap.empty[String, RateLimiter]

def getRateLimiter(host: String): RateLimiter = {
  hostLimiters.getOrElseUpdate(host, RateLimiter.create(10.0))
}

def makeRequest(host: String, request: Request): Response = {
  val limiter = getRateLimiter(host)
  limiter.acquire() // Wait for permit
  sendRequest(host, request)
}

Implementation Details

Key Design Decisions

  • Separate blocking/non-blocking logic: tryReservePermits (non-blocking) doesn't allow token bucket to go negative, while reservePermits (blocking) can reserve future tokens
  • Atomic state management: Uses AtomicReference[RateLimiterState] with immutable case class for thread safety
  • Configurable burst size: Defaults to permitsPerSecond but can be customized
  • Cross-platform sleep: Uses Compat.sleep for millisecond-level precision across platforms

Test Coverage

  • 22 comprehensive tests covering basic functionality, edge cases, concurrency, and time-based behavior
  • Manual ticker support for deterministic testing
  • Parameter validation and error handling

Documentation

Added comprehensive documentation to airframe-control.md with:

  • Basic usage examples
  • Burst control configuration
  • Integration patterns with retry mechanisms
  • Per-host rate limiting implementation

The implementation provides a foundation for maintaining average traffic rates and implementing retry budgets as mentioned in the original issue, complementing the existing retry and circuit breaker functionality in airframe-control.

Fixes #1027.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] airframe-control: RateLimiter airframe-control: Add RateLimiter with token bucket algorithm May 31, 2025
Copilot AI requested a review from xerial May 31, 2025 06:01
Copilot finished work on behalf of xerial May 31, 2025 06:01
@github-actions github-actions Bot added the doc Documentation task label May 31, 2025
Copy link
Copy Markdown
Member

@xerial xerial left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compilation is failing in Scala.js. Fix the error

Comment on lines +173 to +192
test("handle concurrent access") {
val limiter = RateLimiter.create(100.0) // High rate to avoid blocking in test
val counter = new AtomicInteger(0)
val threads = 10
val acquisitionsPerThread = 10

val threadList = (1 to threads).map { _ =>
new Thread(() => {
for (_ <- 1 to acquisitionsPerThread) {
limiter.acquire()
counter.incrementAndGet()
}
})
}

threadList.foreach(_.start())
threadList.foreach(_.join())

counter.get() shouldBe threads * acquisitionsPerThread
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

java.lang.Thread doesn't exist in Scala.js. If this test is important, move this test to .jvm folder

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the concurrent access test that uses java.lang.Thread to a new JVM-specific test file RateLimiterJVMTest.scala in the .jvm/src/test/scala directory. This resolves the Scala.js compilation issue while preserving the important concurrency test for JVM environments. Commit: 98a26ae

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 31, 2025

Compilation is failing in Scala.js. Fix the error

Fixed the Scala.js compilation error by moving the Thread-based concurrent test to a JVM-specific test file. The test now runs only on JVM while maintaining cross-platform compatibility for all other RateLimiter functionality. Commit: 98a26ae

Copilot finished work on behalf of xerial May 31, 2025 06:30
@xerial
Copy link
Copy Markdown
Member

xerial commented May 31, 2025

The code is not formatted correctly. Use ./sbt scalafmtAll

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 31, 2025

The code is not formatted correctly. Use ./sbt scalafmtAll

Fixed code formatting by running ./sbt scalafmtAll as requested. The formatter corrected comment indentation and added proper file ending. Commit: 160c8e8

Copilot finished work on behalf of xerial May 31, 2025 06:45
@xerial xerial marked this pull request as ready for review May 31, 2025 07:02
@xerial xerial merged commit 72e0f1f into main May 31, 2025
17 checks passed
@xerial xerial deleted the copilot/fix-1027-2 branch May 31, 2025 07:11
@xerial xerial added the feature label Jun 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc Documentation task feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

airframe-control: RateLimiter

2 participants