Skip to content

Commit 8872a16

Browse files
committed
Add Jobserver::Pool class
Add a class that implements a Jobserver pool of job slots. This will later be used by Ninja to because a jobserver pool itself for its own spawned sub-processes. This CL includes a Posix and a Win32 implementation + some unit-tests.
1 parent ba77b09 commit 8872a16

File tree

4 files changed

+342
-1
lines changed

4 files changed

+342
-1
lines changed

src/jobserver-posix.cc

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,136 @@ class PosixJobserverClient : public Jobserver::Client {
171171
int write_fd_ = -1;
172172
};
173173

174+
class PosixJobserverPool : public Jobserver::Pool {
175+
public:
176+
std::string GetEnvMakeFlagsValue() const override {
177+
std::string result;
178+
if (!fifo_.empty()) {
179+
result.resize(fifo_.size() + 32);
180+
int ret = snprintf(const_cast<char*>(result.data()), result.size(),
181+
" -j%zd --jobserver-auth=fifo:%s", job_count_,
182+
fifo_.c_str());
183+
if (ret < 0 || ret > static_cast<int>(result.size()))
184+
Fatal("Could not format PosixJobserverPool MAKEFLAGS!");
185+
result.resize(static_cast<size_t>(ret));
186+
}
187+
if (read_fd_ >= 0 && write_fd_ >= 0) {
188+
result.resize(256);
189+
// See technical note in jobserver.c for formatting justification.
190+
int ret = snprintf(const_cast<char*>(result.data()), result.size(),
191+
" -j%zu --jobserver-fds=%d,%d --jobserver-auth=%d,%d",
192+
job_count_, read_fd_, write_fd_, read_fd_, write_fd_);
193+
if (ret < 0 || ret > static_cast<int>(result.size()))
194+
Fatal("Could not format PosixJobserverPool MAKEFLAGS!");
195+
result.resize(static_cast<size_t>(ret));
196+
}
197+
return result;
198+
}
199+
200+
virtual ~PosixJobserverPool() {
201+
if (read_fd_ >= 0)
202+
::close(read_fd_);
203+
if (write_fd_ >= 0)
204+
::close(write_fd_);
205+
if (!fifo_.empty())
206+
::unlink(fifo_.c_str());
207+
}
208+
209+
bool InitWithPipe(size_t slot_count, std::string* error) {
210+
// Create anonymous pipe, then write job slot tokens into it.
211+
int fds[2] = { -1, -1 };
212+
int ret = pipe(fds);
213+
if (ret < 0) {
214+
*error =
215+
std::string("Could not create anonymous pipe: ") + strerror(errno);
216+
return false;
217+
}
218+
219+
// The file descriptors returned by pipe() are already heritable and
220+
// blocking, which is exactly what's needed here.
221+
read_fd_ = fds[0];
222+
write_fd_ = fds[1];
223+
224+
return FillSlots(slot_count, error);
225+
}
226+
227+
bool InitWithFifo(size_t slot_count, std::string* error) {
228+
const char* tmp_dir = getenv("TMPDIR");
229+
if (!tmp_dir)
230+
tmp_dir = "/tmp";
231+
232+
fifo_.resize(strlen(tmp_dir) + 32);
233+
int len = snprintf(const_cast<char*>(fifo_.data()), fifo_.size(),
234+
"%s/NinjaFIFO%d", tmp_dir, getpid());
235+
if (len < 0) {
236+
*error = "Cannot create fifo path!";
237+
return false;
238+
}
239+
fifo_.resize(static_cast<size_t>(len));
240+
241+
int ret = mknod(fifo_.c_str(), S_IFIFO | 0666, 0);
242+
if (ret < 0) {
243+
*error = std::string("Cannot create fifo: ") + strerror(errno);
244+
return false;
245+
}
246+
247+
do {
248+
write_fd_ = ::open(fifo_.c_str(), O_RDWR | O_CLOEXEC);
249+
} while (write_fd_ < 0 && errno == EINTR);
250+
if (write_fd_ < 0) {
251+
*error = std::string("Could not open fifo: ") + strerror(errno);
252+
// Let destructor remove the fifo.
253+
return false;
254+
}
255+
256+
return FillSlots(slot_count, error);
257+
}
258+
259+
private:
260+
// Fill the pool to satisfy |slot_count| job slots. This
261+
// writes |slot_count - 1| bytes to the pipe to satisfy the
262+
// implicit job slot requirement.
263+
bool FillSlots(size_t slot_count, std::string* error) {
264+
job_count_ = slot_count;
265+
for (; slot_count > 1; --slot_count) {
266+
// Write '+' into the pipe, just like GNU Make. Note that some
267+
// implementations write '|' instead, but so far no client or pool
268+
// implementation cares about the exact value, though the official spec
269+
// says this might change in the future.
270+
const char slot_char = '+';
271+
int ret = ::write(write_fd_, &slot_char, 1);
272+
if (ret != 1) {
273+
if (ret < 0 && errno == EINTR)
274+
continue;
275+
*error =
276+
std::string("Could not fill job slots pool: ") + strerror(errno);
277+
return false;
278+
}
279+
}
280+
return true;
281+
}
282+
283+
// Number of parallel job slots (including implicit one).
284+
size_t job_count_ = 0;
285+
286+
// In pipe mode, these are inheritable read and write descriptors for the
287+
// pipe. In fifo mode, read_fd_ will be -1, and write_fd_ will be a
288+
// non-inheritable descriptor to keep the FIFO alive.
289+
int read_fd_ = -1;
290+
int write_fd_ = -1;
291+
292+
// Path to fifo, this will be empty when using an anonymous pipe.
293+
std::string fifo_;
294+
};
295+
174296
} // namespace
175297

176298
// static
177299
std::unique_ptr<Jobserver::Client> Jobserver::Client::Create(
178300
const Jobserver::Config& config, std::string* error) {
179301
bool success = false;
180-
auto client = std::unique_ptr<PosixJobserverClient>(new PosixJobserverClient);
302+
auto client =
303+
std::unique_ptr<PosixJobserverClient>(new PosixJobserverClient());
181304
if (config.mode == Jobserver::Config::kModePipe) {
182305
success = client->InitWithPipeFds(config.read_fd, config.write_fd, error);
183306
} else if (config.mode == Jobserver::Config::kModePosixFifo) {
@@ -189,3 +312,25 @@ std::unique_ptr<Jobserver::Client> Jobserver::Client::Create(
189312
client.reset();
190313
return client;
191314
}
315+
316+
// static
317+
std::unique_ptr<Jobserver::Pool> Jobserver::Pool::Create(
318+
size_t num_job_slots, Jobserver::Config::Mode mode, std::string* error) {
319+
std::unique_ptr<PosixJobserverPool> pool;
320+
if (num_job_slots < 2) {
321+
*error = "At least 2 job slots needed";
322+
return pool;
323+
}
324+
bool success = false;
325+
pool.reset(new PosixJobserverPool());
326+
if (mode == Jobserver::Config::kModePipe) {
327+
success = pool->InitWithPipe(num_job_slots, error);
328+
} else if (mode == Jobserver::Config::kModePosixFifo) {
329+
success = pool->InitWithFifo(num_job_slots, error);
330+
} else {
331+
*error = "Unsupported jobserver mode";
332+
}
333+
if (!success)
334+
pool.reset();
335+
return pool;
336+
}

src/jobserver-win32.cc

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,61 @@ class Win32JobserverClient : public Jobserver::Client {
8686
HANDLE handle_ = NULL;
8787
};
8888

89+
class Win32JobserverPool : public Jobserver::Pool {
90+
public:
91+
bool InitWithSemaphore(size_t slot_count, std::string* error) {
92+
job_count_ = slot_count;
93+
sem_name_ = GetSemaphoreName();
94+
LONG count = static_cast<LONG>(slot_count - 1);
95+
handle_ = ::CreateSemaphoreA(NULL, count, count, sem_name_.c_str());
96+
if (!IsValid()) {
97+
*error = "Could not create semaphore: " + GetLastErrorString();
98+
return false;
99+
}
100+
return true;
101+
}
102+
103+
std::string GetEnvMakeFlagsValue() const override {
104+
std::string result;
105+
result.resize(sem_name_.size() + 32);
106+
int ret =
107+
snprintf(const_cast<char*>(result.data()), result.size(),
108+
" -j%zd --jobserver-auth=%s", job_count_, sem_name_.c_str());
109+
if (ret < 0 || ret > static_cast<int>(result.size()))
110+
Fatal("Could not format Win32JobserverPool MAKEFLAGS!");
111+
112+
return result;
113+
}
114+
115+
virtual ~Win32JobserverPool() {
116+
if (IsValid())
117+
::CloseHandle(handle_);
118+
}
119+
120+
private:
121+
// CreateSemaphore returns NULL on failure.
122+
bool IsValid() const { return handle_ != NULL; }
123+
124+
// Compute semaphore name for new instance.
125+
static std::string GetSemaphoreName() {
126+
static int counter = 0;
127+
counter += 1;
128+
char name[64];
129+
snprintf(name, sizeof(name), "ninja_jobserver_pool_%d_%d",
130+
GetCurrentProcessId(), counter);
131+
return std::string(name);
132+
}
133+
134+
// Semaphore handle.
135+
HANDLE handle_ = NULL;
136+
137+
// Saved slot count.
138+
size_t job_count_ = 0;
139+
140+
// Semaphore name.
141+
std::string sem_name_;
142+
};
143+
89144
} // namespace
90145

91146
// static
@@ -103,3 +158,23 @@ std::unique_ptr<Jobserver::Client> Jobserver::Client::Create(
103158
client.reset();
104159
return client;
105160
}
161+
162+
// static
163+
std::unique_ptr<Jobserver::Pool> Jobserver::Pool::Create(
164+
size_t num_job_slots, Jobserver::Config::Mode mode, std::string* error) {
165+
if (num_job_slots < 2) {
166+
*error = "At least 2 job slots needed";
167+
return nullptr;
168+
}
169+
bool success;
170+
auto pool = std::unique_ptr<Win32JobserverPool>(new Win32JobserverPool());
171+
if (mode == Jobserver::Config::kModeWin32Semaphore) {
172+
success = pool->InitWithSemaphore(num_job_slots, error);
173+
} else {
174+
*error = "Unsupported jobserver mode";
175+
success = false;
176+
}
177+
if (!success)
178+
pool.reset(nullptr);
179+
return pool;
180+
}

src/jobserver.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,4 +232,36 @@ struct Jobserver {
232232
protected:
233233
Client() = default;
234234
};
235+
236+
/// Jobserver::Pool implements a jobserver pool of job slots according
237+
/// to the GNU Make protocol. Usage is the following:
238+
///
239+
/// - Use Create() method to create new instances.
240+
///
241+
/// - Retrieve the value of the MAKEFLAGS environment variable, and
242+
/// ensure it is passed to each client.
243+
///
244+
class Pool {
245+
public:
246+
/// Destructor.
247+
virtual ~Pool() {}
248+
249+
/// Default implementation mode for the current platform.
250+
#ifdef _WIN32
251+
static constexpr Config::Mode kDefaultMode = Config::kModeWin32Semaphore;
252+
#else // !_WIN32
253+
static constexpr Config::Mode kDefaultMode = Config::kModePipe;
254+
#endif // !_WIN32
255+
256+
/// Create new instance to use |num_slots| job slots, using a specific
257+
/// implementation mode. On failure, set |*error| and return null.
258+
///
259+
/// Note that it is an error to use a value of |num_slots| that is <= 1.
260+
static std::unique_ptr<Pool> Create(size_t num_job_slots, Config::Mode mode,
261+
std::string* error);
262+
263+
/// Return the value of the MAKEFLAGS variable, corresponding to this
264+
/// instance, to pass to sub-processes.
265+
virtual std::string GetEnvMakeFlagsValue() const = 0;
266+
};
235267
};

src/jobserver_test.cc

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,4 +402,93 @@ TEST(Jobserver, PosixFifoClientWithWrongPath) {
402402
EXPECT_FALSE(error.empty());
403403
EXPECT_EQ("Empty fifo path", error);
404404
}
405+
#endif // _WIN32
406+
407+
TEST(Jobserver, DefaultPool) {
408+
const size_t kSlotCount = 10;
409+
std::string error;
410+
auto pool = Jobserver::Pool::Create(kSlotCount,
411+
Jobserver::Config::kModeDefault, &error);
412+
ASSERT_TRUE(pool.get()) << error;
413+
EXPECT_TRUE(error.empty());
414+
415+
std::string makeflags = pool->GetEnvMakeFlagsValue();
416+
#ifdef _WIN32
417+
std::string auth_prefix = " -j10 --jobserver-auth=";
418+
#else // !_WIN32
419+
std::string auth_prefix = " -j10 --jobserver-fds=";
420+
#endif // !_WIN32
421+
ASSERT_EQ(auth_prefix, makeflags.substr(0, auth_prefix.size()));
422+
423+
// Parse the MAKEFLAGS value to create a JobServer::Config
424+
Jobserver::Config config;
425+
ASSERT_TRUE(
426+
Jobserver::ParseMakeFlagsValue(makeflags.c_str(), &config, &error));
427+
EXPECT_EQ(config.mode, Jobserver::Config::kModeDefault);
428+
429+
// Create a client from the Config, and try to read all slots.
430+
std::unique_ptr<Jobserver::Client> client =
431+
Jobserver::Client::Create(config, &error);
432+
EXPECT_TRUE(client.get());
433+
EXPECT_TRUE(error.empty()) << error;
434+
435+
// First slot is always implicit.
436+
Jobserver::Slot slot = client->TryAcquire();
437+
EXPECT_TRUE(slot.IsValid());
438+
EXPECT_TRUE(slot.IsImplicit());
439+
440+
// Then read kSlotCount - 1 slots from the pipe.
441+
for (size_t n = 1; n < kSlotCount; ++n) {
442+
Jobserver::Slot slot = client->TryAcquire();
443+
EXPECT_TRUE(slot.IsValid()) << "Slot #" << n + 1;
444+
EXPECT_TRUE(slot.IsExplicit()) << "Slot #" << n + 1;
445+
}
446+
447+
// Pool should be empty now, so next TryAcquire() will fail.
448+
slot = client->TryAcquire();
449+
EXPECT_FALSE(slot.IsValid());
450+
}
451+
452+
#ifndef _WIN32
453+
TEST(Jobserver, PosixFifoPool) {
454+
const size_t kSlotCount = 10;
455+
std::string error;
456+
auto pool = Jobserver::Pool::Create(
457+
kSlotCount, Jobserver::Config::kModePosixFifo, &error);
458+
ASSERT_TRUE(pool.get()) << error;
459+
EXPECT_TRUE(error.empty());
460+
461+
std::string makeflags = pool->GetEnvMakeFlagsValue();
462+
463+
std::string auth_prefix = " -j10 --jobserver-auth=fifo:";
464+
ASSERT_EQ(auth_prefix, makeflags.substr(0, auth_prefix.size()));
465+
466+
// Parse the MAKEFLAGS value to create a JobServer::Config
467+
Jobserver::Config config;
468+
ASSERT_TRUE(
469+
Jobserver::ParseMakeFlagsValue(makeflags.c_str(), &config, &error));
470+
EXPECT_EQ(config.mode, Jobserver::Config::kModePosixFifo);
471+
472+
// Create a client from the Config, and try to read all slots.
473+
std::unique_ptr<Jobserver::Client> client =
474+
Jobserver::Client::Create(config, &error);
475+
EXPECT_TRUE(client.get());
476+
EXPECT_TRUE(error.empty()) << error;
477+
478+
// First slot is always implicit.
479+
Jobserver::Slot slot = client->TryAcquire();
480+
EXPECT_TRUE(slot.IsValid());
481+
EXPECT_TRUE(slot.IsImplicit());
482+
483+
// Then read kSlotCount - 1 slots from the pipe.
484+
for (size_t n = 1; n < kSlotCount; ++n) {
485+
Jobserver::Slot slot = client->TryAcquire();
486+
EXPECT_TRUE(slot.IsValid()) << "Slot #" << n + 1;
487+
EXPECT_TRUE(slot.IsExplicit()) << "Slot #" << n + 1;
488+
}
489+
490+
// Pool should be empty now, so next TryAcquire() will fail.
491+
slot = client->TryAcquire();
492+
EXPECT_FALSE(slot.IsValid());
493+
}
405494
#endif // !_WIN32

0 commit comments

Comments
 (0)