본문으로 건너뛰기
Folly Code Review · 16/89

folly::ManualExecutor — 결정적 테스트를 위한 수동 진행

· Hawk · 3분 읽기

한 줄 요약: ManualExecutor는 task를 queue에 쌓아두고 run() 호출 시에만 실행한다. 비동기 Future 체인의 진행을 step-by-step으로 검증하는 테스트 도구다.

#동기 — 비동기 코드의 결정적 테스트

비동기 코드의 테스트는 race로 깨지기 쉽다. CPUThreadPoolExecutor로 schedule하면 언제 실행되는지 테스트가 통제할 수 없다.

TEST(Worker, ProcessesQueue) {
folly::CPUThreadPoolExecutor pool(2);
Worker w(&pool);
w.enqueue(1);
w.enqueue(2);
// sleep으로 기다림 — flaky
std::this_thread::sleep_for(std::chrono::milliseconds(100));
EXPECT_EQ(w.processed(), 2);
}

위 테스트는 CI 부하에 따라 깨진다. ManualExecutorcallback이 도는 시점을 테스트가 통제하게 한다.

#API

#include <folly/executors/ManualExecutor.h>
folly::ManualExecutor exec;
exec.add([] { std::cout << "task 1\n"; });
exec.add([] { std::cout << "task 2\n"; });
// 아직 출력 없음 — queue에만 쌓임
exec.run(); // 1번 schedule된 만큼 실행
// "task 1"
// "task 2"

추가 메서드.

class ManualExecutor : public DrivableExecutor, public ScheduledExecutor {
public:
void add(Func) override;
void scheduleAt(Func, TimePoint) override;
size_t run(); // ready task 모두 실행, 갯수 반환
size_t drain(); // run을 더 이상 task가 없을 때까지
size_t step(); // ready task 1개만 실행
void drive(); // run + nothing to do면 wait
bool empty() const;
size_t numPending() const;
void advance(Duration); // 가상 시간 진행 — scheduled task 실행
};

#결정적 step-by-step 테스트

TEST(FutureChain, RunsInOrder) {
folly::ManualExecutor exec;
std::vector<int> log;
folly::makeSemiFuture(1)
.via(&exec)
.thenValue([&](int x) { log.push_back(x); return x + 1; })
.thenValue([&](int x) { log.push_back(x); return x + 1; })
.thenValue([&](int x) { log.push_back(x); });
EXPECT_EQ(exec.numPending(), 1); // 첫 callback만 ready
exec.step();
EXPECT_EQ(log, std::vector<int>{1});
exec.step();
EXPECT_EQ(log, std::vector<int>{1, 2});
exec.run(); // 나머지 모두
EXPECT_EQ(log, (std::vector{1, 2, 3}));
}

체인의 각 단계가 언제 실행되는지 명시적으로 통제된다. race가 없으므로 어떤 환경에서도 동일하게 동작한다.

#가상 시간 — advance

TEST(Timer, FiresAtCorrectTime) {
folly::ManualExecutor exec;
std::atomic<bool> fired{false};
exec.scheduleAt(
[&] { fired = true; },
exec.now() + std::chrono::seconds(5));
exec.run();
EXPECT_FALSE(fired); // 아직 5초 안 지남
exec.advance(std::chrono::seconds(4));
EXPECT_FALSE(fired);
exec.advance(std::chrono::seconds(1));
EXPECT_TRUE(fired);
}

advance(d)가상 시간을 d만큼 앞으로 옮기고 그 사이 expire된 schedule을 실행한다. 실제로 5초 wait 하지 않는다.

#drive — Future가 완료될 때까지 진행

auto sf = computeAsync();
auto f = std::move(sf).via(&exec);
// Future 완료까지 진행
while (!f.isReady()) {
exec.drive();
}
auto v = std::move(f).get();

drive()ready task를 실행하고 없으면 short wait 후 다시 본다. loopOnce()와 비슷한 의미다.

#내부 구현

// folly/executors/ManualExecutor.h (요약)
class ManualExecutor : public DrivableExecutor, public ScheduledExecutor {
public:
void add(Func f) override {
std::lock_guard<std::mutex> lock(lock_);
funcs_.push(std::move(f));
sem_.post();
}
size_t run() {
size_t count = 0;
std::vector<Func> funcs;
{
std::lock_guard<std::mutex> lock(lock_);
funcs_.swap_to_vec(funcs); // 현재 시점의 ready만
}
for (auto& f : funcs) {
f();
++count;
}
return count;
}
private:
std::mutex lock_;
std::queue<Func> funcs_;
std::priority_queue<ScheduledFunc> scheduledFuncs_;
TimePoint now_;
folly::LifoSem sem_;
};

queue + scheduled queue + virtual clock의 단순한 조합이다.

#InlineExecutor와의 차이

항목InlineExecutorManualExecutor
실행 시점add() 즉시run()/drive() 호출 시
결정성있음 (즉시)있음 (수동)
가상 시간없음advance() 지원
사용 사례단순 동기 테스트비동기 시퀀스 테스트

InlineExecutor는 비동기성을 제거하지만 ManualExecutor는 비동기성을 시각화한다. 후자가 비동기 시나리오의 테스트에 더 적합하다.

#실전 — RPC 테스트

TEST(RpcClient, RetriesOnTimeout) {
folly::ManualExecutor exec;
MockServer server;
RpcClient client(&exec, &server);
auto sf = client.call("foo");
auto f = std::move(sf).via(&exec);
// 첫 시도
exec.run();
EXPECT_EQ(server.callCount(), 1);
// 5초 후 timeout
exec.advance(std::chrono::seconds(5));
exec.run();
EXPECT_EQ(server.callCount(), 2); // 재시도
// 응답 도착
server.respond(2, "ok");
exec.run();
EXPECT_TRUE(f.isReady());
EXPECT_EQ(std::move(f).get(), "ok");
}

advance타이머 발화 시점을 직접 제어한다. 5초를 실제로 기다리지 않고 타임아웃 시나리오를 검증한다.

#코드 리뷰 포인트

  • 테스트에서 sleep_for를 쓰고 있는가? ManualExecutor + advance로 대체한다.
  • production code에 ManualExecutor가 들어가지 않았는가? 테스트 전용이다.
  • run() vs drain() 사용 의도? 부분 진행이면 step/run, 끝까지면 drain.
  • drive() 무한 루프? Future가 영원히 ready가 안 되는 버그를 가린다. timeout을 둔다.

#자주 보는 안티패턴

// 1. ManualExecutor + 실제 sleep
exec.add([] { ... });
std::this_thread::sleep_for(std::chrono::seconds(1));
exec.run(); // sleep은 의미 없음 — ManualExecutor가 시간 통제
// 2. production에서 ManualExecutor
folly::ManualExecutor exec;
server.setExecutor(&exec); // 누군가 run()을 안 부르면 영원히 진행 안 함
// 3. step()을 부르지 않고 .get()
auto f = compute().via(&exec);
f.get(); // ManualExecutor는 자동 실행 안 함 — deadlock
// 4. advance 없이 timer 테스트
exec.scheduleAt([&] { fired = true; }, exec.now() + std::chrono::seconds(5));
exec.run(); // 가상 시간 안 옮김 — 아무 일도 안 일어남

#정리

  • ManualExecutor는 task를 큐에 쌓고 run()/step() 호출 시에만 실행한다.
  • 비동기 Future 체인의 진행 시점을 테스트가 통제한다.
  • advance(d)로 가상 시간을 옮겨 timer/timeout 시나리오를 결정적으로 테스트한다.
  • production 코드에 들어가면 callback이 영원히 실행되지 않을 위험이 있다.
  • race 없는 단위 테스트의 핵심 도구다.

#다음 편

Part 3-05: EventBase에서 libevent loop의 핵심을 본다.

#관련 항목

Folly Code Review · 17 of 89

  1. 1 Folly Code Review — Meta의 production-grade C++ 라이브러리 코드 분석
  2. 2 Folly 개요 — Meta가 production에서 검증한 utility 모음 분석
  3. 3 Folly vs Abseil 철학 비교 — performance-first vs std-compatible
  4. 4 Folly 빌드와 fbcode 환경 — monorepo의 그림자
  5. 5 Folly API stability 정책 — 어떤 보장도 없다는 솔직함
  6. 6 Folly production validation 문화 — peta-scale에서 단련된 코드
  7. 7 folly::Future 분석 — std::future의 한계를 넘는 composable async
  8. 8 folly::Promise·makeFuture — Future를 만드는 두 길
  9. 9 folly::SemiFuture vs Future — executor binding의 명시화
  10. 10 folly::Future thenValue·thenError·thenTry — continuation 체인 분석
  11. 11 folly::collect·collectAll·collectAny — fan-in 패턴 분석
  12. 12 folly::Future retry·window·via — 제어 흐름 조합자
  13. 13 folly::fibers 분석 — M:N stackful coroutine
  14. 14 folly::InlineExecutor — 호출자 thread에서 즉시 실행
  15. 15 folly::CPUThreadPoolExecutor — CPU-bound 작업의 표준 thread pool
  16. 16 folly::IOThreadPoolExecutor — libevent 기반 I/O pool
  17. 17 folly::ManualExecutor — 결정적 테스트를 위한 수동 진행
  18. 18 folly::EventBase 분석 — libevent 이벤트 루프의 핵심
  19. 19 folly::IOBuf 분석 — zero-copy buffer chain의 기본 단위
  20. 20 folly::IOBufQueue — chain의 push/pull 추상화
  21. 21 folly::io::Cursor·RWCursor — chain 위의 stream
  22. 22 folly Zero-copy 패턴 — IOBuf로 ScatterGather I/O 표현
  23. 23 folly::IOBuf shared semantics — clone·unshare·takeOwnership
  24. 24 folly::FBString 분석 — SSO + COW 구현
  25. 25 folly의 fmt::format 통합 — 모던 포맷팅 채택
  26. 26 folly::StringPiece — string_view 호환 분석
  27. 27 folly Join·Split utilities — 문자열 분해와 결합
  28. 28 folly::to·tryTo — text↔num 변환 분석
  29. 29 folly Conv Customization — 사용자 타입 지원
  30. 30 folly Conv 성능 비교 — sprintf·stringstream 대비
  31. 31 folly::F14ValueMap vs std::unordered_map
  32. 32 folly::F14NodeMap — stable pointer가 필요할 때
  33. 33 folly::F14VectorMap — cache-friendly iteration
  34. 34 folly::F14FastMap — auto-select 동작
  35. 35 folly F14 internals — SIMD probing 메커니즘
  36. 36 folly::small_vector — inline storage 분석
  37. 37 folly::FixedString — compile-time string
  38. 38 folly::AtomicHashMap — lock-free read 분석
  39. 39 folly::ConcurrentHashMap — sharded 동시 해시 맵
  40. 40 folly::EvictingCacheMap — LRU 구현 분석
  41. 41 folly::Synchronized — lock wrapper 패턴
  42. 42 folly::SharedMutex 분석
  43. 43 folly::Baton — one-shot wait 동기화
  44. 44 folly::RWSpinLock 분석
  45. 45 folly::PicoSpinLock — 1-byte spinlock
  46. 46 folly::ProducerConsumerQueue — SPSC 큐 분석
  47. 47 folly::MPMCQueue — multi-producer multi-consumer
  48. 48 folly::UnboundedQueue — 동적 크기 lock-free
  49. 49 folly::fibers::Channel — Go-like channel
  50. 50 folly::dynamic — JSON-like dynamic type 분석
  51. 51 folly JSON conversion — toJson·parseJson
  52. 52 folly dynamic ↔ struct — manual marshaling
  53. 53 folly dynamic Visitor pattern — type별 분기
  54. 54 folly::Singleton vs Meyers/static — 왜 Folly의 Singleton인가
  55. 55 folly::SingletonVault 분석 — 등록·소멸·의존성
  56. 56 folly::Singleton try_get·try_get_fast — TLS-cached 접근
  57. 57 folly::ExceptionWrapper — type-erased exception holder
  58. 58 folly::ScopeGuard·SCOPE_EXIT — RAII cleanup
  59. 59 folly::Optional vs std::optional
  60. 60 folly::Function vs std::function
  61. 61 folly::Lazy — 지연 초기화 wrapper
  62. 62 folly Meta 스타일 code review 패턴
  63. 63 folly anti-patterns — 잘못 쓰면 std보다 느림
  64. 64 folly vs std 선택 기준 분석
  65. 65 folly::coro 개요 — production C++20 코루틴 어댑터
  66. 66 folly::coro::Task — lazy single-shot 코루틴
  67. 67 folly::coro::AsyncGenerator — 비동기 스트림
  68. 68 folly coro blockingWait·collectAll — 동기 경계와 fan-in
  69. 69 folly::coro::Baton·Mutex — 코루틴-aware 동기화
  70. 70 folly::Expected — 결과 또는 오류
  71. 71 folly::Try — Future 결과 wrapper
  72. 72 folly::Try vs Expected 선택 기준
  73. 73 folly::Range — 일반 iterator pair
  74. 74 folly::Uri — URL 파서
  75. 75 folly Fingerprint64·128 — 분산 hash
  76. 76 folly SpookyHashV2 — fast non-crypto hash
  77. 77 folly::Init — main() 부트스트랩
  78. 78 folly::Indestructible — global lifetime 패턴
  79. 79 folly::MicroLock — 1-byte 락
  80. 80 folly::MicroSpinLock — 가장 좁은 spin lock
  81. 81 folly::format — legacy formatter 분석
  82. 82 folly::demangle — typeid 디망글링
  83. 83 folly::DynamicConverter — dynamic ↔ struct
  84. 84 folly::RecordIO — append-only 로그 파일 포맷
  85. 85 folly::io::Compression — zstd·lz4·snappy wrapper
  86. 86 folly::AsyncIO — io_uring·Linux AIO
  87. 87 folly::CancellationToken — 코루틴·Future 취소 전파
  88. 88 folly::observer — hot config의 atomic refresh
  89. 89 fbcode 패턴 모음 — folly 사용의 실전