-
Notifications
You must be signed in to change notification settings - Fork 229
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(memstore): account for clock drift during latency measurement (#1943
) Adds one additional metric negativeIngestionPipelineLatency to record negative ingestion pipeline latencies (as their absolute values). Additionally, values recorded into the two usual ingestion latency metrics are now wrapped inside Math.max(0, value) calls to prevent exceptions.
- Loading branch information
1 parent
166ea1b
commit 0764fa6
Showing
3 changed files
with
156 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package filodb.core | ||
|
||
import scala.concurrent.duration.Duration | ||
|
||
/** | ||
* Rate-limiter utility. | ||
* @param period a "successful" attempt will be indicated only after a full | ||
* period has elapsed since the previous success. | ||
*/ | ||
class RateLimiter(period: Duration) { | ||
private var lastSuccessMillis = 0L; | ||
|
||
/** | ||
* Returns true to indicate an attempt was "successful", else it was "failed". | ||
* Successes are returned only after a full period has elapsed since the previous success. | ||
* | ||
* NOTE: this operation is not thread-safe, but if at least one concurrent invocation is | ||
* successful, then one of the successful timestamps will be recorded internally as the | ||
* most-recent success. In practice, this means async calls may succeed in bursts (which | ||
* may be acceptable in some use-cases). | ||
*/ | ||
def attempt(): Boolean = { | ||
val nowMillis = System.currentTimeMillis() | ||
if (nowMillis - lastSuccessMillis > period.toMillis) { | ||
lastSuccessMillis = nowMillis | ||
return true | ||
} | ||
false | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package filodb.core | ||
|
||
import org.scalatest.funspec.AnyFunSpec | ||
import org.scalatest.matchers.should.Matchers | ||
|
||
|
||
import java.util.concurrent.atomic.AtomicInteger | ||
import java.util.concurrent.{Executors, TimeUnit} | ||
import scala.concurrent.duration.Duration | ||
|
||
class RateLimiterSpec extends AnyFunSpec with Matchers { | ||
it("should apply rate-limits accordingly") { | ||
val rateLimiter = new RateLimiter(Duration(2, TimeUnit.SECONDS)) | ||
|
||
// First attempt should succeed. | ||
rateLimiter.attempt() shouldEqual true | ||
|
||
// Others before a full period has elapsed should fail. | ||
rateLimiter.attempt() shouldEqual false | ||
rateLimiter.attempt() shouldEqual false | ||
rateLimiter.attempt() shouldEqual false | ||
|
||
// Wait the period... | ||
Thread.sleep(2000) | ||
|
||
// Next attempt should succeed. | ||
rateLimiter.attempt() shouldEqual true | ||
|
||
// Again, attempts should fail until a period has elapsed. | ||
rateLimiter.attempt() shouldEqual false | ||
rateLimiter.attempt() shouldEqual false | ||
rateLimiter.attempt() shouldEqual false | ||
} | ||
|
||
it("should reasonably rate-limit concurrent threads") { | ||
val nThreads = 100 | ||
val period = Duration(1, TimeUnit.SECONDS) | ||
val nPeriods = 5 | ||
|
||
val rateLimiter = new RateLimiter(period) | ||
val pool = Executors.newFixedThreadPool(nThreads) | ||
|
||
// All threads will try to increment the time-appropriate counter. | ||
// At the end of the test, there should be at least one counted per period, | ||
// and no single counter should exceed the count of threads (i.e. at least one thread | ||
// was paused long enough that it updated the RateLimiter's internal timestamp | ||
// to something in the previous period. | ||
val periodCounters = (0 until nPeriods).map(_ => new AtomicInteger(0)) | ||
|
||
// Prep the runnable (some of these variables are updated later). | ||
var startMillis = -1L | ||
var isStarted = false | ||
var isShutdown = false | ||
val runnable: Runnable = () => { | ||
while (!isStarted) { | ||
Thread.sleep(500) | ||
} | ||
while (!isShutdown) { | ||
if (rateLimiter.attempt()) { | ||
val iPeriod = (System.currentTimeMillis() - startMillis) / period.toMillis | ||
periodCounters(iPeriod.toInt).incrementAndGet() | ||
} | ||
} | ||
} | ||
|
||
// Kick off the threads and start the test. | ||
for (i <- 0 until nThreads) { | ||
pool.submit(runnable) | ||
} | ||
startMillis = System.currentTimeMillis() | ||
isStarted = true | ||
|
||
// Wait for all periods to elapse. | ||
Thread.sleep(nPeriods * period.toMillis) | ||
|
||
// Shutdown and wait for everything to finish. | ||
isShutdown = true | ||
pool.shutdown() | ||
while (!pool.isTerminated) { | ||
Thread.sleep(1000) | ||
} | ||
|
||
periodCounters.forall(_.get() > 0) shouldEqual true | ||
periodCounters.map(_.get()).max <= nThreads | ||
|
||
// Typical local "println(periodCounters)" output: | ||
// Vector(1, 34, 1, 1, 1) | ||
// Vector(1, 20, 1, 1, 1) | ||
// Vector(1, 13, 1, 2, 1) | ||
} | ||
} |