mirror of
https://github.com/apache/thrift.git
synced 2026-10-29 17:05:46 +08:00
Client: cpp macOS rejects an RLIMIT_AS limit with EINVAL, so the child always exited with 2 and the case failed there. The child now exits with 77 in that case and the case reports that it has nothing to check; any other setrlimit failure still fails it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1097 lines
42 KiB
C++
1097 lines
42 KiB
C++
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
#define BOOST_TEST_MODULE TNonblockingServerTest
|
|
#include <boost/test/unit_test.hpp>
|
|
#include <atomic>
|
|
#include <climits>
|
|
#include <csignal>
|
|
#include <fstream>
|
|
#include <memory>
|
|
#include <string>
|
|
|
|
#include "thrift/TConfiguration.h"
|
|
#include "thrift/concurrency/Monitor.h"
|
|
#include "thrift/concurrency/Thread.h"
|
|
#include "thrift/concurrency/ThreadManager.h"
|
|
#include "thrift/protocol/TBinaryProtocol.h"
|
|
#include "thrift/server/TNonblockingServer.h"
|
|
#include "thrift/transport/TNonblockingServerSocket.h"
|
|
#include "thrift/transport/TSocket.h"
|
|
#include "thrift/transport/TTransportUtils.h"
|
|
|
|
#include <cerrno>
|
|
#include <new>
|
|
|
|
#ifndef _WIN32
|
|
#include <sys/resource.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include "gen-cpp/ParentService.h"
|
|
|
|
#include <event.h>
|
|
|
|
using apache::thrift::concurrency::Guard;
|
|
using apache::thrift::concurrency::Monitor;
|
|
using apache::thrift::concurrency::Mutex;
|
|
using apache::thrift::concurrency::ThreadFactory;
|
|
using apache::thrift::concurrency::Runnable;
|
|
using apache::thrift::concurrency::Thread;
|
|
using apache::thrift::concurrency::ThreadFactory;
|
|
using apache::thrift::server::TServerEventHandler;
|
|
using std::make_shared;
|
|
using std::shared_ptr;
|
|
|
|
using namespace apache::thrift;
|
|
|
|
struct Handler : public test::ParentServiceIf {
|
|
void addString(const std::string& s) override { strings_.push_back(s); }
|
|
void getStrings(std::vector<std::string>& _return) override { _return = strings_; }
|
|
std::vector<std::string> strings_;
|
|
|
|
// dummy overrides not used in this test
|
|
int32_t incrementGeneration() override { return 0; }
|
|
int32_t getGeneration() override { return 0; }
|
|
void getDataWait(std::string&, const int32_t) override {}
|
|
void onewayWait() override {}
|
|
void exceptionWait(const std::string&) override {}
|
|
void unexpectedExceptionWait(const std::string&) override {}
|
|
};
|
|
|
|
class Fixture {
|
|
private:
|
|
struct ListenEventHandler : public TServerEventHandler {
|
|
public:
|
|
ListenEventHandler(Mutex* mutex) : listenMonitor_(mutex), ready_(false) {}
|
|
|
|
void preServe() override /* override */ {
|
|
Guard g(listenMonitor_.mutex());
|
|
ready_ = true;
|
|
listenMonitor_.notify();
|
|
}
|
|
|
|
Monitor listenMonitor_;
|
|
bool ready_;
|
|
};
|
|
|
|
struct Runner : public Runnable {
|
|
int port;
|
|
shared_ptr<event_base> userEventBase;
|
|
shared_ptr<TProcessor> processor;
|
|
shared_ptr<concurrency::ThreadManager> threadManager;
|
|
shared_ptr<server::TNonblockingServer> server;
|
|
shared_ptr<ListenEventHandler> listenHandler;
|
|
shared_ptr<transport::TNonblockingServerSocket> socket;
|
|
shared_ptr<TConfiguration> configuration;
|
|
Mutex mutex_;
|
|
|
|
Runner() {
|
|
port = 0;
|
|
listenHandler.reset(new ListenEventHandler(&mutex_));
|
|
}
|
|
|
|
void run() override {
|
|
// When binding to explicit port, allow retrying to workaround bind failures on ports in use
|
|
int retryCount = port ? 10 : 0;
|
|
startServer(retryCount);
|
|
}
|
|
|
|
void readyBarrier() {
|
|
// block until server is listening and ready to accept connections
|
|
Guard g(mutex_);
|
|
while (!listenHandler->ready_) {
|
|
listenHandler->listenMonitor_.wait();
|
|
}
|
|
}
|
|
private:
|
|
void startServer(int retry_count) {
|
|
try {
|
|
socket.reset(new transport::TNonblockingServerSocket(port));
|
|
if (threadManager) {
|
|
server.reset(new server::TNonblockingServer(
|
|
processor, make_shared<protocol::TBinaryProtocolFactory>(), socket, threadManager));
|
|
} else {
|
|
server.reset(new server::TNonblockingServer(processor, socket));
|
|
}
|
|
if (configuration) {
|
|
server->setConfiguration(configuration);
|
|
}
|
|
server->setServerEventHandler(listenHandler);
|
|
if (userEventBase) {
|
|
server->registerEvents(userEventBase.get());
|
|
}
|
|
server->serve();
|
|
} catch (const transport::TTransportException&) {
|
|
if (retry_count > 0) {
|
|
++port;
|
|
startServer(retry_count - 1);
|
|
} else {
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
struct EventDeleter {
|
|
void operator()(event_base* p) { event_base_free(p); }
|
|
};
|
|
|
|
protected:
|
|
Fixture() : processor(make_shared<test::ParentServiceProcessor>(make_shared<Handler>())) {}
|
|
|
|
~Fixture() {
|
|
if (server) {
|
|
server->stop();
|
|
}
|
|
if (thread) {
|
|
thread->join();
|
|
}
|
|
// Drain the thread pool while the server is still alive. server is declared
|
|
// after threadManager_, so it is destroyed first, and ~TNonblockingServer
|
|
// deletes every TConnection and the IO threads with their notification
|
|
// pipe. A task still unwinding at that point would call notifyIOThread() --
|
|
// and then close() -- on a deleted connection, which it holds by raw
|
|
// pointer. ThreadManager::stop() returns only once every worker has
|
|
// finished the task in its hands, so afterwards nothing is left to touch
|
|
// the objects about to be destroyed.
|
|
if (threadManager_) {
|
|
threadManager_->stop();
|
|
}
|
|
}
|
|
|
|
void setEventBase(event_base* user_event_base) {
|
|
userEventBase_.reset(user_event_base, EventDeleter());
|
|
}
|
|
|
|
int startServer(int port) {
|
|
shared_ptr<Runner> runner(new Runner);
|
|
runner->port = port;
|
|
runner->processor = processor;
|
|
runner->threadManager = threadManager_;
|
|
runner->userEventBase = userEventBase_;
|
|
runner->configuration = configuration_;
|
|
|
|
shared_ptr<ThreadFactory> threadFactory(
|
|
new ThreadFactory(false));
|
|
thread = threadFactory->newThread(runner);
|
|
thread->start();
|
|
runner->readyBarrier();
|
|
|
|
server = runner->server;
|
|
return runner->port;
|
|
}
|
|
|
|
bool canCommunicate(int serverPort) {
|
|
shared_ptr<transport::TSocket> socket(new transport::TSocket("localhost", serverPort));
|
|
// Without a timeout a server that never answers makes this block until the
|
|
// whole test binary is killed, which reports as one opaque timeout. The
|
|
// value only has to beat that: every call made here is a localhost
|
|
// round-trip of a few bytes.
|
|
socket->setRecvTimeout(10000);
|
|
socket->open();
|
|
test::ParentServiceClient client(make_shared<protocol::TBinaryProtocol>(
|
|
make_shared<transport::TFramedTransport>(socket)));
|
|
client.addString("foo");
|
|
std::vector<std::string> strings;
|
|
client.getStrings(strings);
|
|
return strings.size() == 1 && !(strings[0].compare("foo"));
|
|
}
|
|
|
|
// A raw client that announces a frame of declaredSize (optionally sending
|
|
// bodyBytes of payload) and reports whether the server closed the connection
|
|
// without replying. A receive timeout distinguishes a refusal (the server
|
|
// closed, read returns 0) from acceptance (the server waits for the rest of
|
|
// the frame and the read times out).
|
|
bool serverClosesOnFrame(int serverPort, uint32_t declaredSize, uint32_t bodyBytes) {
|
|
transport::TSocket sock("localhost", serverPort);
|
|
sock.setRecvTimeout(1500);
|
|
sock.open();
|
|
uint32_t netSize = htonl(declaredSize);
|
|
sock.write(reinterpret_cast<uint8_t*>(&netSize), sizeof(netSize));
|
|
if (bodyBytes) {
|
|
std::vector<uint8_t> body(bodyBytes, 0);
|
|
sock.write(body.data(), bodyBytes);
|
|
}
|
|
uint8_t buf[16];
|
|
try {
|
|
return sock.read(buf, sizeof(buf)) == 0;
|
|
} catch (const transport::TTransportException&) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private:
|
|
shared_ptr<event_base> userEventBase_;
|
|
shared_ptr<concurrency::ThreadManager> threadManager_;
|
|
|
|
protected:
|
|
shared_ptr<TConfiguration> configuration_;
|
|
|
|
private:
|
|
|
|
protected:
|
|
// Replaces the processor the fixture builds, and switches the server to the
|
|
// thread-pooled path, which dispatches through Task::run() rather than
|
|
// running the processor on the I/O thread.
|
|
void useThreadPool(const shared_ptr<TProcessor>& replacement) {
|
|
processor = replacement;
|
|
threadManager_ = concurrency::ThreadManager::newSimpleThreadManager(2);
|
|
threadManager_->threadFactory(make_shared<ThreadFactory>());
|
|
threadManager_->start();
|
|
}
|
|
|
|
// Replaces the processor the fixture builds while leaving the server on the
|
|
// inline path, so the request runs on the I/O thread rather than on a worker.
|
|
void setProcessor(const shared_ptr<TProcessor>& replacement) {
|
|
processor = replacement;
|
|
}
|
|
|
|
private:
|
|
shared_ptr<TProcessor> processor;
|
|
protected:
|
|
shared_ptr<server::TNonblockingServer> server;
|
|
private:
|
|
shared_ptr<apache::thrift::concurrency::Thread> thread;
|
|
|
|
};
|
|
|
|
BOOST_AUTO_TEST_SUITE(TNonblockingServerTest)
|
|
|
|
BOOST_FIXTURE_TEST_CASE(get_specified_port, Fixture) {
|
|
int specified_port = startServer(12345);
|
|
BOOST_REQUIRE_GE(specified_port, 12345);
|
|
BOOST_REQUIRE_EQUAL(server->getListenPort(), specified_port);
|
|
BOOST_CHECK(canCommunicate(specified_port));
|
|
|
|
server->stop();
|
|
}
|
|
|
|
BOOST_FIXTURE_TEST_CASE(get_assigned_port, Fixture) {
|
|
int specified_port = startServer(0);
|
|
BOOST_REQUIRE_EQUAL(specified_port, 0);
|
|
int assigned_port = server->getListenPort();
|
|
BOOST_REQUIRE_NE(assigned_port, 0);
|
|
BOOST_CHECK(canCommunicate(assigned_port));
|
|
|
|
server->stop();
|
|
}
|
|
|
|
BOOST_FIXTURE_TEST_CASE(provide_event_base, Fixture) {
|
|
event_base* eb = event_base_new();
|
|
setEventBase(eb);
|
|
startServer(0);
|
|
|
|
// assert that the server works
|
|
BOOST_CHECK(canCommunicate(server->getListenPort()));
|
|
#if LIBEVENT_VERSION_NUMBER > 0x02010400
|
|
// also assert that the event_base is actually used when it's easy
|
|
BOOST_CHECK_GT(event_base_get_num_events(eb, EVENT_BASE_COUNT_ADDED), 0);
|
|
#endif
|
|
}
|
|
|
|
// A server and a client handed the same hostname have to agree on which address it
|
|
// means. Each resolves independently, so any difference in the getaddrinfo() flags
|
|
// they pass leaves the server listening on an address the client never dials: with
|
|
// AI_ADDRCONFIG on one side only, a host that carries ::1 on loopback and no other
|
|
// IPv6 address resolves "localhost" to ::1 for the server and 127.0.0.1 for the
|
|
// client, and the connect is refused. See THRIFT-6191.
|
|
BOOST_AUTO_TEST_CASE(bind_and_connect_agree_on_hostname) {
|
|
transport::TNonblockingServerSocket sock("localhost", 0);
|
|
sock.listen();
|
|
transport::TSocket client("localhost", sock.getListenPort());
|
|
client.open();
|
|
BOOST_CHECK(client.isOpen());
|
|
client.close();
|
|
sock.close();
|
|
}
|
|
|
|
// The frame cap TNonblockingServer applies to every accepted connection lives on
|
|
// the server itself: TConfiguration appears nowhere in TNonblockingServer.{h,cpp},
|
|
// so the default here and the library-wide default are two independent numbers
|
|
// that have already drifted 16x apart once. Pin them together, and pin that the
|
|
// setter still wins, so a caller that needs larger frames keeps its escape hatch.
|
|
BOOST_AUTO_TEST_CASE(default_max_frame_size_matches_configuration) {
|
|
auto socket = make_shared<transport::TNonblockingServerSocket>(0);
|
|
auto processor = make_shared<test::ParentServiceProcessor>(make_shared<Handler>());
|
|
server::TNonblockingServer server(processor, socket);
|
|
|
|
BOOST_CHECK_EQUAL(server.getMaxFrameSize(),
|
|
static_cast<size_t>(TConfiguration::DEFAULT_MAX_FRAME_SIZE));
|
|
|
|
server.setMaxFrameSize(4096);
|
|
BOOST_CHECK_EQUAL(server.getMaxFrameSize(), static_cast<size_t>(4096));
|
|
|
|
// The configuration stores the frame size as int, so a value above INT_MAX
|
|
// clamps rather than wrapping to a negative int (which would read back as a
|
|
// huge size_t and disable the limit).
|
|
server.setMaxFrameSize(static_cast<size_t>(INT_MAX) + 1000);
|
|
BOOST_CHECK_EQUAL(server.getMaxFrameSize(), static_cast<size_t>(INT_MAX));
|
|
}
|
|
|
|
// Fails the first call the way argument deserialization fails when a declared
|
|
// container count does not fit in memory: the exception leaves process(). It
|
|
// cannot be raised from the handler instead, because generated dispatch code
|
|
// catches std::exception around the handler call and answers with a
|
|
// TApplicationException -- the allocation that fails is the one made before
|
|
// that point, reading the arguments.
|
|
struct FailsFirstCallProcessor : public TProcessor {
|
|
explicit FailsFirstCallProcessor(const shared_ptr<TProcessor>& delegate)
|
|
: delegate_(delegate), failed_(false), consumed_(false) {}
|
|
|
|
bool process(shared_ptr<protocol::TProtocol> in,
|
|
shared_ptr<protocol::TProtocol> out,
|
|
void* connectionContext) override {
|
|
// Both pool workers reach this, so the flag has to be claimed atomically:
|
|
// read-then-write let two calls each see it unset and both throw.
|
|
if (!failed_.exchange(true)) {
|
|
// Announce the claim before unwinding, so a caller can wait until the
|
|
// failure has been taken rather than guess.
|
|
{
|
|
Guard g(consumedMonitor_.mutex());
|
|
consumed_ = true;
|
|
consumedMonitor_.notifyAll();
|
|
}
|
|
throw std::bad_alloc();
|
|
}
|
|
return delegate_->process(in, out, connectionContext);
|
|
}
|
|
|
|
// Blocks until some call has consumed the injected failure.
|
|
void awaitFailure() {
|
|
Guard g(consumedMonitor_.mutex());
|
|
while (!consumed_) {
|
|
consumedMonitor_.wait();
|
|
}
|
|
}
|
|
|
|
private:
|
|
shared_ptr<TProcessor> delegate_;
|
|
std::atomic<bool> failed_;
|
|
Monitor consumedMonitor_;
|
|
bool consumed_;
|
|
};
|
|
|
|
// Refuses the first callsToFail requests by letting an exception leave
|
|
// process() -- the way inline request processing fails when a request cannot be
|
|
// handled, e.g. a malformed message or a handler that throws. On the inline
|
|
// path TConnection::transition() logs and closes the connection for every
|
|
// std::exception. Later requests are passed to the real processor, so the
|
|
// server can be shown still serving after the refusals.
|
|
struct FailsFirstNProcessor : public TProcessor {
|
|
FailsFirstNProcessor(int callsToFail, const shared_ptr<TProcessor>& delegate)
|
|
: remaining_(callsToFail), delegate_(delegate) {}
|
|
|
|
bool process(shared_ptr<protocol::TProtocol> in,
|
|
shared_ptr<protocol::TProtocol> out,
|
|
void* connectionContext) override {
|
|
if (remaining_.fetch_sub(1) > 0) {
|
|
throw TException("request refused by the test processor");
|
|
}
|
|
return delegate_->process(in, out, connectionContext);
|
|
}
|
|
|
|
private:
|
|
std::atomic<int> remaining_;
|
|
shared_ptr<TProcessor> delegate_;
|
|
};
|
|
|
|
// A request that cannot be allocated is one request. Task::run() answered
|
|
// std::bad_alloc with exit(1), so any peer able to make one allocation fail
|
|
// ended the process for every other client connected to it -- and a declared
|
|
// container count is enough to try. The inline path in this same file already
|
|
// logs and closes the connection for every std::exception; this pins the
|
|
// thread-pooled path to the same behaviour.
|
|
//
|
|
// If the process does end, this test does not fail an assertion: the binary
|
|
// exits mid-run and takes the whole suite with it.
|
|
//
|
|
// It does not assert anything about the doomed connection. A task that throws
|
|
// writes nothing, and the connection reads an empty write buffer as "no reply
|
|
// was owed" -- so the caller of that one request never hears back. An empty
|
|
// buffer is not the same thing as a oneway call: a void method still owes a
|
|
// reply, and an exception is one of the replies it can owe. That confusion is
|
|
// pre-existing, applies to every exception this catch handles, and differs from
|
|
// the inline path, which closes the connection. Recorded separately.
|
|
BOOST_FIXTURE_TEST_CASE(bad_alloc_does_not_end_the_process, Fixture) {
|
|
auto failing = make_shared<FailsFirstCallProcessor>(
|
|
make_shared<test::ParentServiceProcessor>(make_shared<Handler>()));
|
|
useThreadPool(failing);
|
|
|
|
startServer(0);
|
|
int port = server->getListenPort();
|
|
BOOST_REQUIRE_GT(port, 0);
|
|
|
|
// Send the doomed call without waiting for a reply. A task that throws leaves
|
|
// outputTransport_ empty, and the connection answers an empty buffer with
|
|
// nothing at all -- so a client that waited here would wait for ever. That is
|
|
// the same for every exception this path catches and is not what this test is
|
|
// about; see the note above.
|
|
{
|
|
auto socket = make_shared<transport::TSocket>("localhost", port);
|
|
socket->open();
|
|
test::ParentServiceClient client(
|
|
make_shared<protocol::TBinaryProtocol>(make_shared<transport::TFramedTransport>(socket)));
|
|
client.send_addString("this one cannot be allocated");
|
|
socket->close();
|
|
}
|
|
|
|
// Wait until that call has actually taken the injected failure. The two pool
|
|
// workers are served in whatever order they are scheduled, so without this
|
|
// the call below can reach process() first, consume the failure itself and
|
|
// get no reply -- leaving the doomed call to be served normally and this test
|
|
// waiting on a reply that is never owed.
|
|
failing->awaitFailure();
|
|
|
|
// The finding is that the process does not survive the above. A second
|
|
// connection, served normally, is what says it did.
|
|
BOOST_CHECK_MESSAGE(canCommunicate(port),
|
|
"the server stopped serving after one failed allocation");
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
// The other way one connection used to end the process, and the worse of the
|
|
// two: TConnection::transition() throws std::bad_alloc when the read buffer
|
|
// cannot be grown to the frame size the peer asked for. transition() runs under
|
|
// eventHandler(), which libevent -- a C library -- calls. An exception unwinding
|
|
// out of a C frame reaches std::terminate, not a handler, so the process
|
|
// aborted rather than merely exiting.
|
|
//
|
|
// Run in a child, because making an allocation fail means capping the address
|
|
// space, and that cannot be undone for the rest of the suite. The child exiting
|
|
// 0 is the assertion; before the fix it died on SIGABRT.
|
|
BOOST_AUTO_TEST_CASE(allocation_failure_on_the_io_thread_does_not_end_the_process) {
|
|
pid_t pid = fork();
|
|
BOOST_REQUIRE_NE(pid, -1);
|
|
|
|
if (pid == 0) {
|
|
struct rlimit limit;
|
|
limit.rlim_cur = limit.rlim_max = 256UL * 1024 * 1024;
|
|
if (setrlimit(RLIMIT_AS, &limit) != 0) {
|
|
// Some platforms, macOS among them, do not support an RLIMIT_AS limit at all.
|
|
_exit(errno == EINVAL ? 77 : 2);
|
|
}
|
|
|
|
auto socket = make_shared<transport::TNonblockingServerSocket>(0);
|
|
auto processor = make_shared<test::ParentServiceProcessor>(make_shared<Handler>());
|
|
auto child = make_shared<server::TNonblockingServer>(processor, socket);
|
|
// Above the cap on purpose, so that growing the buffer for the frame below
|
|
// is an allocation that cannot succeed.
|
|
child->setMaxFrameSize(512UL * 1024 * 1024);
|
|
|
|
auto factory = make_shared<ThreadFactory>(false);
|
|
struct Serve : public Runnable {
|
|
shared_ptr<server::TNonblockingServer> server;
|
|
void run() override { server->serve(); }
|
|
};
|
|
auto runnable = make_shared<Serve>();
|
|
runnable->server = child;
|
|
auto serveThread = factory->newThread(runnable);
|
|
serveThread->start();
|
|
|
|
for (int i = 0; i < 100 && child->getListenPort() == 0; ++i) {
|
|
THRIFT_SLEEP_USEC(10000);
|
|
}
|
|
|
|
try {
|
|
transport::TSocket client("localhost", child->getListenPort());
|
|
client.open();
|
|
uint32_t declared = 400UL * 1024 * 1024;
|
|
uint8_t header[4] = {static_cast<uint8_t>(declared >> 24),
|
|
static_cast<uint8_t>(declared >> 16),
|
|
static_cast<uint8_t>(declared >> 8),
|
|
static_cast<uint8_t>(declared)};
|
|
client.write(header, sizeof(header));
|
|
client.flush();
|
|
THRIFT_SLEEP_USEC(1500000);
|
|
client.close();
|
|
} catch (...) {
|
|
_exit(3);
|
|
}
|
|
|
|
// Reached only if the allocation failure did not take the process with it.
|
|
child->stop();
|
|
serveThread->join();
|
|
_exit(0);
|
|
}
|
|
|
|
int status = 0;
|
|
BOOST_REQUIRE_NE(waitpid(pid, &status, 0), -1);
|
|
BOOST_CHECK_MESSAGE(WIFEXITED(status),
|
|
"the server process was killed by a signal rather than exiting");
|
|
if (WIFEXITED(status) && WEXITSTATUS(status) == 77) {
|
|
BOOST_TEST_MESSAGE("RLIMIT_AS is not supported on this platform; nothing to check");
|
|
return;
|
|
}
|
|
if (WIFEXITED(status)) {
|
|
BOOST_CHECK_EQUAL(WEXITSTATUS(status), 0);
|
|
}
|
|
}
|
|
|
|
// A server transport whose first accept() fails the way a transient ::accept()
|
|
// error does. TNonblockingServerSocket turns every ::accept() failure into a
|
|
// TTransportException(UNKNOWN), and that exception is raised from inside
|
|
// libevent's listen callback. libevent is a C library, so an exception unwinding
|
|
// out of the callback reaches std::terminate rather than a handler, and a single
|
|
// such failure used to take the only listener down with it. The listener now
|
|
// logs the one failure and keeps accepting, exactly as the per-connection
|
|
// eventHandler()/notifyHandler() callbacks already do.
|
|
class AcceptFailsOnceServerSocket : public transport::TNonblockingServerSocket {
|
|
public:
|
|
explicit AcceptFailsOnceServerSocket(int port)
|
|
: transport::TNonblockingServerSocket(port), acceptCalls_(0), injectedFailures_(0) {}
|
|
|
|
int acceptCalls() const { return acceptCalls_.load(); }
|
|
int injectedFailures() const { return injectedFailures_.load(); }
|
|
|
|
protected:
|
|
shared_ptr<transport::TSocket> acceptImpl() override {
|
|
++acceptCalls_;
|
|
if (injectedFailures_.load() == 0) {
|
|
++injectedFailures_;
|
|
throw transport::TTransportException(transport::TTransportException::UNKNOWN, "accept()");
|
|
}
|
|
return transport::TNonblockingServerSocket::acceptImpl();
|
|
}
|
|
|
|
private:
|
|
std::atomic<int> acceptCalls_;
|
|
std::atomic<int> injectedFailures_;
|
|
};
|
|
|
|
// Run in a child, because before the fix the process aborts and that cannot be
|
|
// caught. The child reaching _exit(0) is the assertion. It gets there only if
|
|
// exactly one accept failure was injected, a later accept still ran (so the
|
|
// listener did not stop), and a client was then served a full round-trip.
|
|
BOOST_AUTO_TEST_CASE(transient_accept_error_does_not_end_the_server) {
|
|
pid_t pid = fork();
|
|
BOOST_REQUIRE_NE(pid, -1);
|
|
|
|
if (pid == 0) {
|
|
// The forked child must not run the Boost test monitor's signal handling:
|
|
// when the listener stops on the unfixed tree it aborts, and the parent
|
|
// should read that as a clean signal death rather than Boost's recovery path.
|
|
::signal(SIGABRT, SIG_DFL);
|
|
|
|
auto socket = make_shared<AcceptFailsOnceServerSocket>(0);
|
|
auto processor = make_shared<test::ParentServiceProcessor>(make_shared<Handler>());
|
|
auto child = make_shared<server::TNonblockingServer>(processor, socket);
|
|
|
|
struct Serve : public Runnable {
|
|
shared_ptr<server::TNonblockingServer> server;
|
|
void run() override { server->serve(); }
|
|
};
|
|
auto runnable = make_shared<Serve>();
|
|
runnable->server = child;
|
|
auto factory = make_shared<ThreadFactory>(false);
|
|
auto serveThread = factory->newThread(runnable);
|
|
serveThread->start();
|
|
|
|
for (int i = 0; i < 1000 && child->getListenPort() == 0; ++i) {
|
|
THRIFT_SLEEP_USEC(5000);
|
|
}
|
|
if (child->getListenPort() == 0) {
|
|
_exit(2);
|
|
}
|
|
int port = child->getListenPort();
|
|
|
|
// The first accept() this connection triggers is the injected failure.
|
|
// Before the fix the listener is gone by now and this round-trip never
|
|
// completes; after the fix the failure is logged, the level-triggered listen
|
|
// event fires again, the retry accepts the connection, and the call is
|
|
// served.
|
|
bool served = false;
|
|
try {
|
|
auto client = make_shared<transport::TSocket>("localhost", port);
|
|
client->setRecvTimeout(10000);
|
|
client->open();
|
|
test::ParentServiceClient parent(make_shared<protocol::TBinaryProtocol>(
|
|
make_shared<transport::TFramedTransport>(client)));
|
|
parent.addString("after a transient accept error");
|
|
std::vector<std::string> strings;
|
|
parent.getStrings(strings);
|
|
served = (strings.size() == 1 && strings[0] == "after a transient accept error");
|
|
client->close();
|
|
} catch (...) {
|
|
_exit(3);
|
|
}
|
|
|
|
if (!served) {
|
|
_exit(4);
|
|
}
|
|
if (socket->injectedFailures() != 1) {
|
|
_exit(5);
|
|
}
|
|
if (socket->acceptCalls() < 2) {
|
|
_exit(6);
|
|
}
|
|
|
|
child->stop();
|
|
serveThread->join();
|
|
_exit(0);
|
|
}
|
|
|
|
int status = 0;
|
|
BOOST_REQUIRE_NE(waitpid(pid, &status, 0), -1);
|
|
if (WIFSIGNALED(status)) {
|
|
BOOST_ERROR("the server process was killed by signal "
|
|
<< WTERMSIG(status) << "; a transient accept error ended the listener");
|
|
} else {
|
|
BOOST_REQUIRE(WIFEXITED(status));
|
|
BOOST_CHECK_EQUAL(WEXITSTATUS(status), 0);
|
|
}
|
|
}
|
|
|
|
// A server event handler whose createContext() throws the first time. The
|
|
// nonblocking server calls createContext() while setting an accepted connection
|
|
// up (TConnection::init(), reached from handleEvent() on the listener thread),
|
|
// so this exercises the second part of the listener's boundary: a throw after a
|
|
// successful accept() must close that one connection and keep the listener
|
|
// running, not unwind libevent.
|
|
class ThrowsFirstCreateContextHandler : public TServerEventHandler {
|
|
public:
|
|
ThrowsFirstCreateContextHandler() : thrown_(false) {}
|
|
void* createContext(shared_ptr<protocol::TProtocol>,
|
|
shared_ptr<protocol::TProtocol>) override {
|
|
if (!thrown_.exchange(true)) {
|
|
throw std::runtime_error("createContext failed");
|
|
}
|
|
return nullptr;
|
|
}
|
|
std::atomic<bool> thrown_;
|
|
};
|
|
|
|
BOOST_AUTO_TEST_CASE(createContext_failure_does_not_end_the_server) {
|
|
pid_t pid = fork();
|
|
BOOST_REQUIRE_NE(pid, -1);
|
|
|
|
if (pid == 0) {
|
|
::signal(SIGABRT, SIG_DFL);
|
|
|
|
auto socket = make_shared<transport::TNonblockingServerSocket>(0);
|
|
auto processor = make_shared<test::ParentServiceProcessor>(make_shared<Handler>());
|
|
auto child = make_shared<server::TNonblockingServer>(processor, socket);
|
|
auto handler = make_shared<ThrowsFirstCreateContextHandler>();
|
|
child->setServerEventHandler(handler);
|
|
|
|
struct Serve : public Runnable {
|
|
shared_ptr<server::TNonblockingServer> server;
|
|
void run() override { server->serve(); }
|
|
};
|
|
auto runnable = make_shared<Serve>();
|
|
runnable->server = child;
|
|
auto factory = make_shared<ThreadFactory>(false);
|
|
auto serveThread = factory->newThread(runnable);
|
|
serveThread->start();
|
|
|
|
for (int i = 0; i < 1000 && child->getListenPort() == 0; ++i) {
|
|
THRIFT_SLEEP_USEC(5000);
|
|
}
|
|
if (child->getListenPort() == 0) {
|
|
_exit(2);
|
|
}
|
|
int port = child->getListenPort();
|
|
|
|
// The first connection the server sets up throws in createContext(). Before
|
|
// the fix that unwinds libevent and aborts; after it the connection is
|
|
// closed and the listener keeps running. Retry a full round-trip: the first
|
|
// attempt's connection is dropped, a later one is served.
|
|
bool served = false;
|
|
for (int attempt = 0; attempt < 10 && !served; ++attempt) {
|
|
try {
|
|
auto client = make_shared<transport::TSocket>("localhost", port);
|
|
client->setRecvTimeout(5000);
|
|
client->open();
|
|
test::ParentServiceClient parent(make_shared<protocol::TBinaryProtocol>(
|
|
make_shared<transport::TFramedTransport>(client)));
|
|
parent.addString("after a createContext failure");
|
|
std::vector<std::string> strings;
|
|
parent.getStrings(strings);
|
|
served = (strings.size() == 1 && strings[0] == "after a createContext failure");
|
|
client->close();
|
|
} catch (...) {
|
|
THRIFT_SLEEP_USEC(50000);
|
|
}
|
|
}
|
|
|
|
if (!served) {
|
|
_exit(4);
|
|
}
|
|
if (!handler->thrown_.load()) {
|
|
_exit(5);
|
|
}
|
|
|
|
child->stop();
|
|
serveThread->join();
|
|
_exit(0);
|
|
}
|
|
|
|
int status = 0;
|
|
BOOST_REQUIRE_NE(waitpid(pid, &status, 0), -1);
|
|
if (WIFSIGNALED(status)) {
|
|
BOOST_ERROR("the server process was killed by signal "
|
|
<< WTERMSIG(status) << "; a failed connection setup ended the listener");
|
|
} else {
|
|
BOOST_REQUIRE(WIFEXITED(status));
|
|
BOOST_CHECK_EQUAL(WEXITSTATUS(status), 0);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// The server enforces the maximum frame size from a configuration it is given,
|
|
// not only the library default. A frame above the configured maximum -- but far
|
|
// below the 16 MB default the server would otherwise apply -- is refused and the
|
|
// connection closed. Before the server honoured a configuration this frame was
|
|
// accepted and the server waited for its body.
|
|
BOOST_FIXTURE_TEST_CASE(honours_configured_max_frame_size, Fixture) {
|
|
configuration_ = make_shared<TConfiguration>();
|
|
configuration_->setMaxFrameSize(1024);
|
|
startServer(0);
|
|
int port = server->getListenPort();
|
|
|
|
BOOST_CHECK(serverClosesOnFrame(port, 2000, 0));
|
|
|
|
server->stop();
|
|
}
|
|
|
|
// The accepted socket carries the server's configuration, so a frame larger
|
|
// than the configured maximum message size is refused before the read buffer is
|
|
// grown for it, even when it is below the frame-size ceiling. Before the socket
|
|
// carried the configuration its budget was the 100 MB default and this frame was
|
|
// accepted.
|
|
BOOST_FIXTURE_TEST_CASE(honours_configured_max_message_size, Fixture) {
|
|
configuration_ = make_shared<TConfiguration>(1024 /* maxMessageSize */);
|
|
startServer(0);
|
|
int port = server->getListenPort();
|
|
|
|
BOOST_CHECK(serverClosesOnFrame(port, 2000, 0));
|
|
|
|
server->stop();
|
|
}
|
|
|
|
// A generous configuration leaves ordinary traffic untouched: a real framed
|
|
// request still round-trips.
|
|
BOOST_FIXTURE_TEST_CASE(generous_configuration_still_serves, Fixture) {
|
|
configuration_ = make_shared<TConfiguration>();
|
|
startServer(0);
|
|
BOOST_CHECK(canCommunicate(server->getListenPort()));
|
|
|
|
server->stop();
|
|
}
|
|
|
|
// The read buffer grows as the payload arrives rather than being reserved on
|
|
// the header, so a frame much larger than the ~1 KiB initial reservation is
|
|
// assembled over many libevent callbacks, doubling the buffer each time it
|
|
// fills. This drives that path with real traffic: two multi-megabyte requests
|
|
// must round-trip byte-for-byte. The first grows the buffer far past
|
|
// IDLE_READ_BUFFER_LIMIT and then closes, so returnConnection() frees it via
|
|
// checkIdleBufferMemLimit(); every request -- the first included -- begins from
|
|
// a freed-or-fresh (null) buffer and has to regrow it from nothing. A lost or
|
|
// misplaced byte, or a mishandled null buffer, surfaces as a mismatch here. The
|
|
// two payloads use distinct fill bytes so that reading stale bytes left in a
|
|
// recycled buffer would fail the comparison rather than pass by coincidence.
|
|
BOOST_FIXTURE_TEST_CASE(read_buffer_grows_across_callbacks_and_regrows_after_reclaim, Fixture) {
|
|
startServer(0);
|
|
int port = server->getListenPort();
|
|
BOOST_REQUIRE_GT(port, 0);
|
|
|
|
// Far larger than the ~1 KiB initial reservation, so assembling either frame
|
|
// forces repeated growth across callbacks; comfortably under the default
|
|
// frame size limit so the frames themselves are accepted.
|
|
const std::string first(2u * 1024 * 1024, 'a');
|
|
const std::string second(2u * 1024 * 1024, 'b');
|
|
|
|
{
|
|
auto socket = make_shared<transport::TSocket>("localhost", port);
|
|
socket->open();
|
|
test::ParentServiceClient client(
|
|
make_shared<protocol::TBinaryProtocol>(make_shared<transport::TFramedTransport>(socket)));
|
|
client.addString(first);
|
|
std::vector<std::string> strings;
|
|
client.getStrings(strings);
|
|
BOOST_REQUIRE_EQUAL(strings.size(), 1u);
|
|
BOOST_CHECK(strings[0] == first);
|
|
// socket closes here -> the grown (> IDLE_READ_BUFFER_LIMIT) read buffer is
|
|
// freed when the connection is returned to the pool.
|
|
}
|
|
|
|
{
|
|
auto socket = make_shared<transport::TSocket>("localhost", port);
|
|
socket->open();
|
|
test::ParentServiceClient client(
|
|
make_shared<protocol::TBinaryProtocol>(make_shared<transport::TFramedTransport>(socket)));
|
|
client.addString(second);
|
|
std::vector<std::string> strings;
|
|
client.getStrings(strings);
|
|
BOOST_REQUIRE_EQUAL(strings.size(), 2u);
|
|
BOOST_CHECK(strings[0] == first);
|
|
BOOST_CHECK(strings[1] == second);
|
|
}
|
|
|
|
server->stop();
|
|
}
|
|
|
|
// The same growth machinery, but with the read buffer reclaimed *between frames
|
|
// on a live connection* rather than when the connection closes. APP_SEND_RESULT
|
|
// runs checkIdleBufferMemLimit() once every getResizeBufferEveryN() requests;
|
|
// forcing that to 1 frees the buffer grown by the first large request before the
|
|
// second arrives on the same connection. On the single I/O thread the order is
|
|
// fixed -- send the reply, reclaim, then read the next frame -- so the second
|
|
// request is guaranteed to regrow the buffer from empty mid-connection. Both
|
|
// requests round-tripping intact is the assertion.
|
|
BOOST_FIXTURE_TEST_CASE(read_buffer_regrows_after_mid_connection_reclaim, Fixture) {
|
|
startServer(0);
|
|
int port = server->getListenPort();
|
|
BOOST_REQUIRE_GT(port, 0);
|
|
// Reclaim an oversized read buffer after every request instead of every 512,
|
|
// so the free lands between the two frames below rather than at close.
|
|
server->setResizeBufferEveryN(1);
|
|
|
|
const std::string first(2u * 1024 * 1024, 'a');
|
|
const std::string second(2u * 1024 * 1024, 'b');
|
|
|
|
auto socket = make_shared<transport::TSocket>("localhost", port);
|
|
socket->open();
|
|
test::ParentServiceClient client(
|
|
make_shared<protocol::TBinaryProtocol>(make_shared<transport::TFramedTransport>(socket)));
|
|
client.addString(first);
|
|
client.addString(second);
|
|
std::vector<std::string> strings;
|
|
client.getStrings(strings);
|
|
BOOST_REQUIRE_EQUAL(strings.size(), 2u);
|
|
BOOST_CHECK(strings[0] == first);
|
|
BOOST_CHECK(strings[1] == second);
|
|
|
|
server->stop();
|
|
}
|
|
|
|
// Frames that arrive back-to-back must be read one at a time. A client may send
|
|
// its next request before the previous response is back -- a oneway call
|
|
// followed by another call does exactly that -- so the socket can hold more than
|
|
// the frame being read. The read buffer grows in doublings and can be larger
|
|
// than the frame (a fresh buffer rounds up, and a buffer kept from an earlier
|
|
// request may already exceed it), so each read has to stop at the end of the
|
|
// frame rather than the end of the buffer, or it takes bytes of the next
|
|
// request. Both requests go out in a single write, so they are guaranteed to be
|
|
// waiting on the socket together.
|
|
BOOST_FIXTURE_TEST_CASE(back_to_back_frames_are_read_one_at_a_time, Fixture) {
|
|
startServer(0);
|
|
int port = server->getListenPort();
|
|
BOOST_REQUIRE_GT(port, 0);
|
|
|
|
// Serialize two complete framed requests up front; a client would wait for
|
|
// each response before sending the next call.
|
|
auto requests = make_shared<transport::TMemoryBuffer>();
|
|
test::ParentServiceClient writer(
|
|
make_shared<protocol::TBinaryProtocol>(make_shared<transport::TFramedTransport>(requests)));
|
|
writer.send_addString("pipelined");
|
|
const size_t firstFrameSize = requests->getBufferAsString().size();
|
|
writer.send_getStrings();
|
|
const std::string bytes = requests->getBufferAsString();
|
|
// A first frame that exactly fills a power-of-two buffer leaves no room to
|
|
// read past it, and this test would pass without exercising the bound.
|
|
BOOST_REQUIRE_NE(firstFrameSize & (firstFrameSize - 1), 0u);
|
|
|
|
auto socket = make_shared<transport::TSocket>("localhost", port);
|
|
socket->setRecvTimeout(5000);
|
|
socket->open();
|
|
socket->write(reinterpret_cast<const uint8_t*>(bytes.data()),
|
|
static_cast<uint32_t>(bytes.size()));
|
|
|
|
test::ParentServiceClient reader(
|
|
make_shared<protocol::TBinaryProtocol>(make_shared<transport::TFramedTransport>(socket)));
|
|
reader.recv_addString();
|
|
std::vector<std::string> strings;
|
|
reader.recv_getStrings(strings);
|
|
BOOST_REQUIRE_EQUAL(strings.size(), 1u);
|
|
BOOST_CHECK_EQUAL(strings[0], "pipelined");
|
|
|
|
server->stop();
|
|
}
|
|
|
|
#if defined(__linux__)
|
|
// The declared frame size is a number the peer chooses; the read buffer must be
|
|
// grown as the payload arrives, not reserved in full when the four-byte header
|
|
// is read. Here the peer announces a 256 MiB frame and sends none of it, so a
|
|
// server that grows with the payload reserves next to nothing, while one that
|
|
// reserves on the header commits the whole frame. The reservation is address
|
|
// space (std::realloc; committed, not resident), so this reads VmSize from
|
|
// /proc, which is Linux-only. It runs in a child because the unfixed path
|
|
// reserves hundreds of MiB that would otherwise perturb the rest of the suite.
|
|
static long readVmSizeKB() {
|
|
std::ifstream status("/proc/self/status");
|
|
std::string key;
|
|
while (status >> key) {
|
|
if (key == "VmSize:") {
|
|
long kb = -1;
|
|
status >> kb;
|
|
return kb;
|
|
}
|
|
std::getline(status, key);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(read_buffer_grows_with_the_payload_not_the_header) {
|
|
pid_t pid = fork();
|
|
BOOST_REQUIRE_NE(pid, -1);
|
|
|
|
if (pid == 0) {
|
|
auto socket = make_shared<transport::TNonblockingServerSocket>(0);
|
|
auto processor = make_shared<test::ParentServiceProcessor>(make_shared<Handler>());
|
|
auto child = make_shared<server::TNonblockingServer>(processor, socket);
|
|
// Well above the default so the large header below clears the frame-size
|
|
// check and reaches the buffer-growth path.
|
|
child->setMaxFrameSize(512UL * 1024 * 1024);
|
|
|
|
auto factory = make_shared<ThreadFactory>(false);
|
|
struct Serve : public Runnable {
|
|
shared_ptr<server::TNonblockingServer> server;
|
|
void run() override { server->serve(); }
|
|
};
|
|
auto runnable = make_shared<Serve>();
|
|
runnable->server = child;
|
|
auto serveThread = factory->newThread(runnable);
|
|
serveThread->start();
|
|
|
|
for (int i = 0; i < 100 && child->getListenPort() == 0; ++i) {
|
|
THRIFT_SLEEP_USEC(10000);
|
|
}
|
|
|
|
try {
|
|
transport::TSocket client("localhost", child->getListenPort());
|
|
client.open();
|
|
long before = readVmSizeKB();
|
|
uint32_t declared = 256UL * 1024 * 1024;
|
|
uint8_t header[4] = {static_cast<uint8_t>(declared >> 24),
|
|
static_cast<uint8_t>(declared >> 16),
|
|
static_cast<uint8_t>(declared >> 8),
|
|
static_cast<uint8_t>(declared)};
|
|
client.write(header, sizeof(header));
|
|
client.flush();
|
|
// Give the I/O thread time to read the header and act on it.
|
|
THRIFT_SLEEP_USEC(1500000);
|
|
long after = readVmSizeKB();
|
|
client.close();
|
|
child->stop();
|
|
serveThread->join();
|
|
|
|
// The server must not have reserved the declared frame. The unfixed path
|
|
// grows by ~256-512 MiB; a payload-driven one by essentially nothing.
|
|
// 64 MiB of slack covers unrelated allocations without admitting the
|
|
// wrong behaviour.
|
|
long grewKB = (before < 0 || after < 0) ? -1 : after - before;
|
|
_exit((grewKB >= 0 && grewKB < 64L * 1024) ? 0 : 1);
|
|
} catch (...) {
|
|
_exit(3);
|
|
}
|
|
}
|
|
|
|
int status = 0;
|
|
BOOST_REQUIRE_NE(waitpid(pid, &status, 0), -1);
|
|
BOOST_REQUIRE(WIFEXITED(status));
|
|
BOOST_CHECK_EQUAL(WEXITSTATUS(status), 0);
|
|
}
|
|
#endif
|
|
|
|
// A request can be refused while it is processed inline on the I/O thread: a
|
|
// malformed message or a handler that throws leaves process(), and
|
|
// TConnection::transition() then closes the connection. close() hands the
|
|
// connection object back to the server, which -- once the idle pool is at its
|
|
// limit -- deletes it, or otherwise keeps it for reuse. Once transition() has
|
|
// closed a connection, workSocket() must not read any of its members again. The
|
|
// test drives several connections that each complete a frame whose processing
|
|
// is refused: with the idle pool limited to one, the first refusal pools its
|
|
// connection and every later one is deleted while that pooled connection is
|
|
// still present, so a connection object is returned and deleted at the moment
|
|
// workSocket() would otherwise inspect it to decide whether to keep reading.
|
|
// The server has to keep serving a well-formed client afterwards. Built with
|
|
// AddressSanitizer, the unfixed tree reports an error in workSocket(); with the
|
|
// fix the run is clean and the final client round-trips.
|
|
BOOST_FIXTURE_TEST_CASE(connection_refused_during_processing_is_not_touched_again, Fixture) {
|
|
const int kConnections = 4;
|
|
|
|
// The first kConnections completed frames are refused (the processor throws);
|
|
// anything after is served normally, so the final client round-trips.
|
|
auto failing = make_shared<FailsFirstNProcessor>(
|
|
kConnections, make_shared<test::ParentServiceProcessor>(make_shared<Handler>()));
|
|
setProcessor(failing);
|
|
|
|
startServer(0);
|
|
int port = server->getListenPort();
|
|
BOOST_REQUIRE_GT(port, 0);
|
|
|
|
// Delete a returned connection as soon as one other sits idle in the pool, so
|
|
// the refusals below free connection objects rather than pooling them all.
|
|
server->setConnectionStackLimit(1);
|
|
|
|
// Open every connection first and wait until the server has accepted all of
|
|
// them with an empty idle pool. Only then are frames completed, so each
|
|
// refusal returns its connection while the others are still active. If a
|
|
// frame were completed before all connections existed, each new connection
|
|
// would reuse the single pooled object and the pool would never hold one
|
|
// connection while another is returned.
|
|
std::vector<shared_ptr<transport::TSocket>> sockets;
|
|
for (int i = 0; i < kConnections; ++i) {
|
|
auto sock = make_shared<transport::TSocket>("localhost", port);
|
|
sock->setRecvTimeout(5000);
|
|
sock->open();
|
|
sockets.push_back(sock);
|
|
}
|
|
for (int i = 0; i < 500
|
|
&& server->getNumActiveConnections() < static_cast<size_t>(kConnections);
|
|
++i) {
|
|
THRIFT_SLEEP_USEC(10000);
|
|
}
|
|
BOOST_REQUIRE_EQUAL(server->getNumActiveConnections(), static_cast<size_t>(kConnections));
|
|
BOOST_REQUIRE_EQUAL(server->getNumIdleConnections(), static_cast<size_t>(0));
|
|
|
|
// Complete a frame on every connection. Each one reaches the processor, is
|
|
// refused, and the connection is closed and returned -- the first pooled, the
|
|
// rest deleted while the pooled one is still present.
|
|
const uint32_t body = 8;
|
|
std::vector<uint8_t> payload(body, 0);
|
|
for (auto& sock : sockets) {
|
|
uint32_t netSize = htonl(body);
|
|
sock->write(reinterpret_cast<uint8_t*>(&netSize), sizeof(netSize));
|
|
sock->write(payload.data(), body);
|
|
sock->flush();
|
|
}
|
|
|
|
// Keep the clients in step with the server: each connection is closed by the
|
|
// server once its frame is refused, so the reads drain to end-of-stream.
|
|
for (auto& sock : sockets) {
|
|
uint8_t buf[16];
|
|
try {
|
|
while (sock->read(buf, sizeof(buf)) > 0) {
|
|
}
|
|
} catch (const transport::TTransportException&) {
|
|
}
|
|
sock->close();
|
|
}
|
|
|
|
// The refusals did not stop the server: a fresh, well-formed client is served.
|
|
BOOST_CHECK(canCommunicate(port));
|
|
|
|
server->stop();
|
|
}
|
|
|
|
BOOST_AUTO_TEST_SUITE_END()
|