[llvm][Support] Add ExponentialBackoff helper (#81206)

This provides a simple way to implement exponential backoff using a do
while loop.

Usage example (also see the change to LockFileManager.cpp):
```
ExponentialBackoff Backoff(10s);
do {
  if (tryToDoSomething())
    return ItWorked;
} while (Backoff.waitForNextAttempt());
return Timeout;
```

Abstracting this out of `LockFileManager` as the module build daemon
will need it.
This commit is contained in:
Michael Spencer
2024-02-13 18:35:39 -08:00
committed by GitHub
parent 22d2f3aa30
commit edff3ff4d3
6 changed files with 134 additions and 31 deletions

View File

@@ -0,0 +1,65 @@
//===- llvm/Support/ExponentialBackoff.h ------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file defines a helper class for implementing exponential backoff.
//
//===----------------------------------------------------------------------===//
#ifndef LLVM_EXPONENTIALBACKOFF_H
#define LLVM_EXPONENTIALBACKOFF_H
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/Error.h"
#include <chrono>
#include <random>
namespace llvm {
/// A class to help implement exponential backoff.
///
/// Example usage:
/// \code
/// ExponentialBackoff Backoff(10s);
/// do {
/// if (tryToDoSomething())
/// return ItWorked;
/// } while (Backoff.waitForNextAttempt());
/// return Timeout;
/// \endcode
class ExponentialBackoff {
public:
using duration = std::chrono::steady_clock::duration;
using time_point = std::chrono::steady_clock::time_point;
/// \param Timeout the maximum wall time this should run for starting when
/// this object is constructed.
/// \param MinWait the minimum amount of time `waitForNextAttempt` will sleep
/// for.
/// \param MaxWait the maximum amount of time `waitForNextAttempt` will sleep
/// for.
ExponentialBackoff(duration Timeout,
duration MinWait = std::chrono::milliseconds(10),
duration MaxWait = std::chrono::milliseconds(500))
: MinWait(MinWait), MaxWait(MaxWait),
EndTime(std::chrono::steady_clock::now() + Timeout) {}
/// Blocks while waiting for the next attempt.
/// \returns true if you should try again, false if the timeout has been
/// reached.
bool waitForNextAttempt();
private:
duration MinWait;
duration MaxWait;
time_point EndTime;
std::random_device RandDev;
int64_t CurrentMultiplier = 1;
};
} // end namespace llvm
#endif // LLVM_EXPONENTIALBACKOFF_H

View File

@@ -176,6 +176,7 @@ add_llvm_component_library(LLVMSupport
ELFAttributes.cpp
Error.cpp
ErrorHandling.cpp
ExponentialBackoff.cpp
ExtensibleRTTI.cpp
FileCollector.cpp
FileUtilities.cpp

View File

@@ -0,0 +1,29 @@
//===- llvm/Support/ExponentialBackoff.h ------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "llvm/Support/ExponentialBackoff.h"
#include <thread>
using namespace llvm;
bool ExponentialBackoff::waitForNextAttempt() {
auto Now = std::chrono::steady_clock::now();
if (Now >= EndTime)
return false;
duration CurMaxWait = std::min(MinWait * CurrentMultiplier, MaxWait);
std::uniform_int_distribution<uint64_t> Dist(MinWait.count(),
CurMaxWait.count());
// Use random_device directly instead of a PRNG as uniform_int_distribution
// often only takes a few samples anyway.
duration WaitDuration = std::min(duration(Dist(RandDev)), EndTime - Now);
if (CurMaxWait < MaxWait)
CurrentMultiplier *= 2;
std::this_thread::sleep_for(WaitDuration);
return true;
}

View File

@@ -11,6 +11,7 @@
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/Errc.h"
#include "llvm/Support/ErrorOr.h"
#include "llvm/Support/ExponentialBackoff.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Process.h"
@@ -20,7 +21,6 @@
#include <chrono>
#include <ctime>
#include <memory>
#include <random>
#include <sys/stat.h>
#include <sys/types.h>
#include <system_error>
@@ -295,29 +295,15 @@ LockFileManager::waitForUnlock(const unsigned MaxSeconds) {
return Res_Success;
// Since we don't yet have an event-based method to wait for the lock file,
// implement randomized exponential backoff, similar to Ethernet collision
// use randomized exponential backoff, similar to Ethernet collision
// algorithm. This improves performance on machines with high core counts
// when the file lock is heavily contended by multiple clang processes
const unsigned long MinWaitDurationMS = 10;
const unsigned long MaxWaitMultiplier = 50; // 500ms max wait
unsigned long WaitMultiplier = 1;
unsigned long ElapsedTimeSeconds = 0;
using namespace std::chrono_literals;
ExponentialBackoff Backoff(std::chrono::seconds(MaxSeconds), 10ms, 500ms);
std::random_device Device;
std::default_random_engine Engine(Device());
auto StartTime = std::chrono::steady_clock::now();
do {
// Wait first as this is only called when the lock is known to be held.
while (Backoff.waitForNextAttempt()) {
// FIXME: implement event-based waiting
// Sleep for the designated interval, to allow the owning process time to
// finish up and remove the lock file.
std::uniform_int_distribution<unsigned long> Distribution(1,
WaitMultiplier);
unsigned long WaitDurationMS = MinWaitDurationMS * Distribution(Engine);
std::this_thread::sleep_for(std::chrono::milliseconds(WaitDurationMS));
if (sys::fs::access(LockFileName.c_str(), sys::fs::AccessMode::Exist) ==
errc::no_such_file_or_directory) {
// If the original file wasn't created, somone thought the lock was dead.
@@ -329,17 +315,7 @@ LockFileManager::waitForUnlock(const unsigned MaxSeconds) {
// If the process owning the lock died without cleaning up, just bail out.
if (!processStillExecuting((*Owner).first, (*Owner).second))
return Res_OwnerDied;
WaitMultiplier *= 2;
if (WaitMultiplier > MaxWaitMultiplier) {
WaitMultiplier = MaxWaitMultiplier;
}
ElapsedTimeSeconds = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::steady_clock::now() - StartTime)
.count();
} while (ElapsedTimeSeconds < MaxSeconds);
}
// Give up.
return Res_Timeout;

View File

@@ -38,6 +38,7 @@ add_llvm_unittest(SupportTests
ErrnoTest.cpp
ErrorOrTest.cpp
ErrorTest.cpp
ExponentialBackoffTest.cpp
ExtensibleRTTITest.cpp
FileCollectorTest.cpp
FileOutputBufferTest.cpp

View File

@@ -0,0 +1,31 @@
//===- unittests/ExponentialBackoffTest.cpp -------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "llvm/Support/ExponentialBackoff.h"
#include "gtest/gtest.h"
#include <chrono>
using namespace llvm;
using namespace std::chrono_literals;
namespace {
TEST(ExponentialBackoffTest, Timeout) {
auto Start = std::chrono::steady_clock::now();
// Use short enough times that this test runs quickly.
ExponentialBackoff Backoff(100ms, 1ms, 10ms);
do {
} while (Backoff.waitForNextAttempt());
auto Duration = std::chrono::steady_clock::now() - Start;
EXPECT_GE(Duration, 100ms);
}
// Testing individual wait duration is omitted as those tests would be
// non-deterministic.
} // end anonymous namespace