-
Notifications
You must be signed in to change notification settings - Fork 78
[ETCM-266]-replaced-rate-limiter-built-on-twitter #873
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
Changes from all commits
cf27856
b04bc3d
f7e0a69
59dc8df
16bddee
10fb166
d9584ca
c61ec05
405a76a
202f5ac
ec001ba
0929963
4df7be8
711867d
c645eed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,25 +1,71 @@ | ||
| package io.iohk.ethereum.jsonrpc.server.http | ||
|
|
||
| import java.time.Clock | ||
| import java.time.Duration | ||
|
|
||
| import akka.http.scaladsl.model.RemoteAddress | ||
| import com.twitter.util.LruMap | ||
| import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpServer.JsonRpcHttpServerConfig | ||
| import akka.NotUsed | ||
| import akka.http.scaladsl.model.{RemoteAddress, StatusCodes} | ||
| import akka.http.scaladsl.server.{Directive0, Route} | ||
| import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpServer.RateLimitConfig | ||
| import akka.http.scaladsl.server.Directives._ | ||
| import com.google.common.base.Ticker | ||
| import com.google.common.cache.CacheBuilder | ||
| import io.iohk.ethereum.jsonrpc.JsonRpcError | ||
| import de.heikoseeberger.akkahttpjson4s.Json4sSupport | ||
| import io.iohk.ethereum.jsonrpc.serialization.JsonSerializers | ||
| import org.json4s.{DefaultFormats, Formats, Serialization, native} | ||
|
|
||
| trait RateLimit { | ||
| class RateLimit(config: RateLimitConfig) extends Directive0 with Json4sSupport { | ||
|
|
||
| val config: JsonRpcHttpServerConfig | ||
| private implicit val serialization: Serialization = native.Serialization | ||
| private implicit val formats: Formats = DefaultFormats + JsonSerializers.RpcErrorJsonSerializer | ||
|
|
||
| val latestRequestTimestamps = new LruMap[RemoteAddress, Long](config.rateLimit.latestTimestampCacheSize) | ||
| private[this] lazy val minInterval = config.minRequestInterval.toSeconds | ||
|
|
||
| val clock: Clock = Clock.systemUTC() | ||
| private[this] lazy val lru = { | ||
| val nanoDuration = config.minRequestInterval.toNanos | ||
| val javaDuration = Duration.ofNanos(nanoDuration) | ||
| val ticker: Ticker = new Ticker { | ||
| override def read(): Long = getCurrentTimeNanos | ||
| } | ||
| CacheBuilder | ||
| .newBuilder() | ||
| .weakKeys() | ||
| .expireAfterAccess(javaDuration) | ||
| .ticker(ticker) | ||
| .build[RemoteAddress, NotUsed]() | ||
| } | ||
|
|
||
| private[this] def isBelowRateLimit(ip: RemoteAddress): Boolean = { | ||
| var exists = true | ||
| lru.get( | ||
| ip, | ||
| () => { | ||
| exists = false | ||
| NotUsed | ||
| } | ||
| ) | ||
| exists | ||
| } | ||
|
|
||
| def isBelowRateLimit(clientAddress: RemoteAddress): Boolean = { | ||
| val timeMillis = clock.instant().toEpochMilli | ||
| val latestRequestTimestamp = latestRequestTimestamps.getOrElse(clientAddress, 0L) | ||
| // Override this to test | ||
| protected def getCurrentTimeNanos: Long = System.nanoTime() | ||
|
|
||
| val response = latestRequestTimestamp + config.rateLimit.minRequestInterval.toMillis < timeMillis | ||
| if (response) latestRequestTimestamps.put(clientAddress, timeMillis) | ||
| response | ||
| // Such algebras prevent if-elseif-else boilerplate in the JsonRPCServer code | ||
| // It is also guaranteed that: | ||
| // 1) no IP address is extracted unless config.enabled is true | ||
| // 2) no LRU is created unless config.enabled is true | ||
| // 3) cache is accessed only once (using get) | ||
| override def tapply(f: Unit => Route): Route = { | ||
| if (config.enabled) { | ||
| extractClientIP { ip => | ||
| if (isBelowRateLimit(ip)) { | ||
| val err = JsonRpcError.RateLimitError(minInterval) | ||
| complete((StatusCodes.TooManyRequests, err)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a minor comment, we can reduce this line:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually it was inline in the beginning, but I've decided that the code is quite unreadable :) |
||
| } else { | ||
| f.apply(()) | ||
| } | ||
| } | ||
| } else f.apply(()) | ||
| } | ||
|
|
||
biandratti marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.