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