Commit cb274c8e authored by octal's avatar octal

Improved the client architecture and fixed a data-race in async

parent e703fc45
...@@ -15,28 +15,32 @@ int main() { ...@@ -15,28 +15,32 @@ int main() {
Http::Experimental::Client client("http://supnetwork.org:9080"); Http::Experimental::Client client("http://supnetwork.org:9080");
auto opts = Http::Experimental::Client::options() auto opts = Http::Experimental::Client::options()
.threads(1) .threads(1)
.maxConnections(20); .maxConnections(64);
using namespace Net::Http; using namespace Net::Http;
constexpr size_t Requests = 5000; constexpr size_t Requests = 10000;
std::atomic<int> responsesReceived(0); std::atomic<int> responsesReceived(0);
client.init(opts); client.init(opts);
for (int i = 0; i < Requests; ++i) { for (int i = 0; i < Requests; ++i) {
client.get(client client.get(client
.request("/ping") .request("/ping")
.cookie(Cookie("FOO", "bar")), std::chrono::milliseconds(1000)) .cookie(Cookie("FOO", "bar")))
.then([&](const Http::Response& response) { .then([&](const Http::Response& response) {
responsesReceived.fetch_add(1); responsesReceived.fetch_add(1);
//std::cout << "code = " << response.code() << std::endl; //std::cout << "code = " << response.code() << std::endl;
// std::cout << "body = " << response.body() << std::endl; //std::cout << "body = " << response.body() << std::endl;
}, Async::NoExcept); }, Async::NoExcept);
const auto count = i + 1;
if (count % 10 == 0)
std::cout << "Sent " << count << " requests" << std::endl;
} }
std::cout << "Sent " << Requests << " requests" << std::endl;
for (;;) { for (;;) {
std::this_thread::sleep_for(std::chrono::seconds(1)); std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Received " << responsesReceived.load() << " responses" << std::endl; auto count = responsesReceived.load();
std::cout << "Received " << count << " responses" << std::endl;
if (count == Requests) break;
} }
client.shutdown(); client.shutdown();
} }
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
#include <memory> #include <memory>
#include <atomic> #include <atomic>
#include <vector> #include <vector>
#include <mutex>
#include "optional.h" #include "optional.h"
#include "typeid.h" #include "typeid.h"
...@@ -170,6 +171,19 @@ namespace Async { ...@@ -170,6 +171,19 @@ namespace Async {
State state; State state;
std::exception_ptr exc; std::exception_ptr exc;
/*
* We need this lock because a Promise might be resolved or rejected from a thread A
* while a continuation to the same Promise (Core) might be attached at the same from
* a thread B. If that's the case, then we need to serialize operations so that we
* avoid a race-condition.
*
* Since we have a lock, we have a blocking progress guarantee but I don't expect this
* to be a major bottleneck as I don't expect major contention on the lock
* If it ends up being a bottlenick, try @improving it by experimenting with a lock-free
* scheme
*/
std::mutex mtx;
std::vector<std::shared_ptr<Request>> requests; std::vector<std::shared_ptr<Request>> requests;
TypeId id; TypeId id;
...@@ -536,10 +550,13 @@ namespace Async { ...@@ -536,10 +550,13 @@ namespace Async {
* than runtime. However, since types are erased, this looks like * than runtime. However, since types are erased, this looks like
* a difficult task * a difficult task
*/ */
if (core_->isVoid()) if (core_->isVoid()) {
throw Error("Attempt to resolve a void promise with arguments"); throw Error("Attempt to resolve a void promise with arguments");
}
std::unique_lock<std::mutex> guard(core_->mtx);
core_->construct<Type>(std::forward<Arg>(arg)); core_->construct<Type>(std::forward<Arg>(arg));
for (const auto& req: core_->requests) { for (const auto& req: core_->requests) {
req->resolve(core_); req->resolve(core_);
} }
...@@ -554,6 +571,7 @@ namespace Async { ...@@ -554,6 +571,7 @@ namespace Async {
if (!core_->isVoid()) if (!core_->isVoid())
throw Error("Attempt ro resolve a non-void promise with no argument"); throw Error("Attempt ro resolve a non-void promise with no argument");
std::unique_lock<std::mutex> guard(core_->mtx);
core_->state = State::Fulfilled; core_->state = State::Fulfilled;
for (const auto& req: core_->requests) { for (const auto& req: core_->requests) {
req->resolve(core_); req->resolve(core_);
...@@ -578,6 +596,7 @@ namespace Async { ...@@ -578,6 +596,7 @@ namespace Async {
if (core_->state != State::Pending) if (core_->state != State::Pending)
throw Error("Attempt to reject a fulfilled promise"); throw Error("Attempt to reject a fulfilled promise");
std::unique_lock<std::mutex> guard(core_->mtx);
core_->exc = std::make_exception_ptr(exc); core_->exc = std::make_exception_ptr(exc);
core_->state = State::Rejected; core_->state = State::Rejected;
for (const auto& req: core_->requests) { for (const auto& req: core_->requests) {
...@@ -716,6 +735,7 @@ namespace Async { ...@@ -716,6 +735,7 @@ namespace Async {
std::forward<ResolveFunc>(resolveFunc), std::forward<ResolveFunc>(resolveFunc),
std::forward<RejectFunc>(rejectFunc))); std::forward<RejectFunc>(rejectFunc)));
std::unique_lock<std::mutex> guard(core_->mtx);
if (isFulfilled()) { if (isFulfilled()) {
req->resolve(core_); req->resolve(core_);
} }
...@@ -723,9 +743,9 @@ namespace Async { ...@@ -723,9 +743,9 @@ namespace Async {
req->reject(core_); req->reject(core_);
} }
core_->requests.push_back(req); core_->requests.push_back(req);
return promise; return promise;
} }
private: private:
......
...@@ -24,12 +24,21 @@ namespace Experimental { ...@@ -24,12 +24,21 @@ namespace Experimental {
class ConnectionPool; class ConnectionPool;
class Transport; class Transport;
struct Connection { struct Connection : public std::enable_shared_from_this<Connection> {
friend class ConnectionPool; friend class ConnectionPool;
typedef std::function<void()> OnDone; typedef std::function<void()> OnDone;
Connection()
: fd(-1)
, connectionState_(NotConnected)
, inflightCount(0)
, responsesReceived(0)
{
state_.store(static_cast<uint32_t>(State::Idle));
}
struct RequestData { struct RequestData {
RequestData( RequestData(
...@@ -48,9 +57,9 @@ struct Connection { ...@@ -48,9 +57,9 @@ struct Connection {
Async::Resolver resolve; Async::Resolver resolve;
Async::Rejection reject; Async::Rejection reject;
Http::Request request;
std::string host; std::string host;
std::chrono::milliseconds timeout; std::chrono::milliseconds timeout;
Http::Request request;
OnDone onDone; OnDone onDone;
}; };
...@@ -65,13 +74,6 @@ struct Connection { ...@@ -65,13 +74,6 @@ struct Connection {
Connected Connected
}; };
Connection()
: fd(-1)
, connectionState_(NotConnected)
{
state_.store(static_cast<uint32_t>(State::Idle));
}
void connect(Net::Address addr); void connect(Net::Address addr);
void close(); void close();
bool isConnected() const; bool isConnected() const;
...@@ -94,15 +96,45 @@ struct Connection { ...@@ -94,15 +96,45 @@ struct Connection {
Fd fd; Fd fd;
void handleResponsePacket(const char* buffer, size_t totalBytes);
void handleTimeout();
std::string dump() const;
private: private:
std::atomic<int> inflightCount;
std::atomic<int> responsesReceived;
struct sockaddr_in saddr;
void processRequestQueue(); void processRequestQueue();
struct RequestEntry {
RequestEntry(
Async::Resolver resolve, Async::Rejection reject,
std::shared_ptr<TimerPool::Entry> timer,
OnDone onDone)
: resolve(std::move(resolve))
, reject(std::move(reject))
, timer(std::move(timer))
, onDone(std::move(onDone))
{ }
Async::Resolver resolve;
Async::Rejection reject;
std::shared_ptr<TimerPool::Entry> timer;
OnDone onDone;
};
std::atomic<uint32_t> state_; std::atomic<uint32_t> state_;
ConnectionState connectionState_; ConnectionState connectionState_;
std::shared_ptr<Transport> transport_; std::shared_ptr<Transport> transport_;
Queue<RequestData> requestsQueue; Queue<RequestData> requestsQueue;
std::deque<RequestEntry> inflightRequests;
Net::TimerPool timerPool_; Net::TimerPool timerPool_;
Private::Parser<Http::Response> parser_;
}; };
struct ConnectionPool { struct ConnectionPool {
...@@ -129,15 +161,12 @@ public: ...@@ -129,15 +161,12 @@ public:
void registerPoller(Polling::Epoll& poller); void registerPoller(Polling::Epoll& poller);
Async::Promise<void> Async::Promise<void>
asyncConnect(Fd fd, const struct sockaddr* address, socklen_t addr_len); asyncConnect(const std::shared_ptr<Connection>& connection, const struct sockaddr* address, socklen_t addr_len);
void asyncSendRequest( Async::Promise<ssize_t> asyncSendRequest(
Fd fd, const std::shared_ptr<Connection>& connection,
std::shared_ptr<TimerPool::Entry> timer, std::shared_ptr<TimerPool::Entry> timer,
const Buffer& buffer, const Buffer& buffer);
Async::Resolver resolve,
Async::Rejection reject,
OnResponseParsed onParsed);
private: private:
...@@ -146,79 +175,61 @@ private: ...@@ -146,79 +175,61 @@ private:
Retry Retry
}; };
struct PendingConnection { struct ConnectionEntry {
PendingConnection( ConnectionEntry(
Async::Resolver resolve, Async::Rejection reject, Async::Resolver resolve, Async::Rejection reject,
Fd fd, const struct sockaddr* addr, socklen_t addr_len) std::shared_ptr<Connection> connection, const struct sockaddr* addr, socklen_t addr_len)
: resolve(std::move(resolve)) : resolve(std::move(resolve))
, reject(std::move(reject)) , reject(std::move(reject))
, fd(fd) , connection(std::move(connection))
, addr(addr) , addr(addr)
, addr_len(addr_len) , addr_len(addr_len)
{ } { }
Async::Resolver resolve; Async::Resolver resolve;
Async::Rejection reject; Async::Rejection reject;
Fd fd; std::shared_ptr<Connection> connection;
const struct sockaddr* addr; const struct sockaddr* addr;
socklen_t addr_len; socklen_t addr_len;
}; };
struct InflightRequest { struct RequestEntry {
InflightRequest( RequestEntry(
Async::Resolver resolve, Async::Rejection reject, Async::Resolver resolve, Async::Rejection reject,
Fd fd, std::shared_ptr<Connection> connection,
std::shared_ptr<TimerPool::Entry> timer, std::shared_ptr<TimerPool::Entry> timer,
const Buffer& buffer, const Buffer& buffer)
OnResponseParsed onParsed = nullptr) : resolve(std::move(resolve))
: resolve_(std::move(resolve))
, reject(std::move(reject)) , reject(std::move(reject))
, fd(fd) , connection(std::move(connection))
, timer(std::move(timer)) , timer(std::move(timer))
, buffer(buffer) , buffer(buffer)
, onParsed(onParsed)
{ {
} }
void feed(const char* buffer, size_t totalBytes) { Async::Resolver resolve;
if (!parser)
parser.reset(new Private::Parser<Http::Response>());
parser->feed(buffer, totalBytes);
}
void resolve(Http::Response response) {
if (onParsed)
onParsed();
resolve_(std::move(response));
}
Async::Resolver resolve_;
Async::Rejection reject; Async::Rejection reject;
Fd fd; std::shared_ptr<Connection> connection;
std::shared_ptr<TimerPool::Entry> timer; std::shared_ptr<TimerPool::Entry> timer;
Buffer buffer; Buffer buffer;
OnResponseParsed onParsed;
std::shared_ptr<Private::Parser<Http::Response>> parser;
}; };
PollableQueue<InflightRequest> requestsQueue; PollableQueue<RequestEntry> requestsQueue;
PollableQueue<PendingConnection> connectionsQueue; PollableQueue<ConnectionEntry> connectionsQueue;
std::unordered_map<Fd, PendingConnection> pendingConnections; std::unordered_map<Fd, ConnectionEntry> connections;
std::unordered_map<Fd, std::deque<InflightRequest>> inflightRequests; std::unordered_map<Fd, RequestEntry> requests;
std::unordered_map<Fd, Fd> timeouts; std::unordered_map<Fd, std::shared_ptr<Connection>> timeouts;
void asyncSendRequestImpl(InflightRequest& req, WriteStatus status = FirstTry); void asyncSendRequestImpl(const RequestEntry& req, WriteStatus status = FirstTry);
void handleRequestsQueue(); void handleRequestsQueue();
void handleConnectionQueue(); void handleConnectionQueue();
void handleIncoming(Fd fd); void handleIncoming(const std::shared_ptr<Connection>& connection);
void handleResponsePacket(Fd fd, const char* buffer, size_t totalBytes); void handleResponsePacket(const std::shared_ptr<Connection>& connection, const char* buffer, size_t totalBytes);
void handleTimeout(Fd fd); void handleTimeout(const std::shared_ptr<Connection>& connection);
}; };
...@@ -274,6 +285,7 @@ public: ...@@ -274,6 +285,7 @@ public:
void shutdown(); void shutdown();
private: private:
Io::ServiceGroup io_; Io::ServiceGroup io_;
std::string url_; std::string url_;
std::string host_; std::string host_;
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
#include <atomic> #include <atomic>
#include <unistd.h> #include <unistd.h>
#include "os.h" #include "os.h"
#include "io.h"
namespace Net { namespace Net {
...@@ -35,6 +36,7 @@ public: ...@@ -35,6 +36,7 @@ public:
Entry() Entry()
: fd(-1) : fd(-1)
, registered(false)
{ {
state.store(static_cast<uint32_t>(State::Idle)); state.store(static_cast<uint32_t>(State::Idle));
} }
...@@ -53,11 +55,19 @@ public: ...@@ -53,11 +55,19 @@ public:
} }
void disarm(); void disarm();
void registerIo(Io::Service* io) {
if (!registered) {
io->registerFd(fd, Polling::NotifyOn::Read);
}
registered = true;
}
private: private:
void armMs(std::chrono::milliseconds value); void armMs(std::chrono::milliseconds value);
enum class State : uint32_t { Idle, Used }; enum class State : uint32_t { Idle, Used };
std::atomic<uint32_t> state; std::atomic<uint32_t> state;
bool registered;
}; };
std::shared_ptr<Entry> pickTimer(); std::shared_ptr<Entry> pickTimer();
......
This diff is collapsed.
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
#include "async.h" #include "async.h"
#include <thread> #include <thread>
#include <algorithm> #include <algorithm>
#include <deque>
#include <mutex>
#include <condition_variable>
Async::Promise<int> doAsync(int N) Async::Promise<int> doAsync(int N)
{ {
...@@ -291,3 +294,139 @@ TEST(async_test, rethrow_test) { ...@@ -291,3 +294,139 @@ TEST(async_test, rethrow_test) {
ASSERT_TRUE(p2.isRejected()); ASSERT_TRUE(p2.isRejected());
} }
template<typename T>
struct MessageQueue {
public:
template<typename U>
void push(U&& arg) {
std::unique_lock<std::mutex> guard(mtx);
q.push_back(std::forward<U>(arg));
cv.notify_one();
}
T pop() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [=]() { return !q.empty(); });
T out = std::move(q.front());
q.pop_front();
return out;
}
bool tryPop(T& out, std::chrono::milliseconds timeout) {
std::unique_lock<std::mutex> lock(mtx);
if (!cv.wait_for(lock, timeout, [=]() { return !q.empty(); }))
return false;
out = std::move(q.front());
q.pop_front();
return true;
}
private:
std::deque<T> q;
std::mutex mtx;
std::condition_variable cv;
};
struct Worker {
public:
~Worker() {
thread->join();
}
void start() {
shutdown.store(false);
thread.reset(new std::thread([=]() { run(); }));
}
void stop() {
shutdown.store(true);
}
Async::Promise<int> doWork(int seq) {
return Async::Promise<int>([=](Async::Resolver& resolve, Async::Rejection& reject) {
queue.push(new WorkRequest(std::move(resolve), std::move(reject), seq));
});
}
private:
void run() {
while (!shutdown) {
WorkRequest *request;
if (queue.tryPop(request, std::chrono::milliseconds(200))) {
request->resolve(request->seq);
delete request;
}
}
}
struct WorkRequest {
WorkRequest(Async::Resolver resolve, Async::Rejection reject, int seq)
: resolve(std::move(resolve))
, reject(std::move(reject))
, seq(seq)
{
}
int seq;
Async::Resolver resolve;
Async::Rejection reject;
};
std::atomic<bool> shutdown;
MessageQueue<WorkRequest*> queue;
std::random_device rd;
std::unique_ptr<std::thread> thread;
};
TEST(async_test, stress_multithreaded_test) {
static constexpr size_t OpsPerThread = 100000;
static constexpr size_t Workers = 6;
static constexpr size_t Ops = OpsPerThread * Workers;
std::cout << "Starting stress testing promises, hang on, this test might take some time to complete" << std::endl;
std::cout << "=================================================" << std::endl;
std::cout << "Parameters for the test: " << std::endl;
std::cout << "Workers -> " << Workers << std::endl;
std::cout << "OpsPerThread -> " << OpsPerThread << std::endl;
std::cout << "Total Ops -> " << Ops << std::endl;
std::cout << "=================================================" << std::endl;
std::cout << std::endl << std::endl;
std::vector<std::unique_ptr<Worker>> workers;
for (size_t i = 0; i < Workers; ++i) {
std::unique_ptr<Worker> wrk(new Worker);
wrk->start();
workers.push_back(std::move(wrk));
}
std::vector<Async::Promise<int>> promises;
std::atomic<int> resolved(0);
size_t wrkIndex = 0;
for (size_t i = 0; i < Ops; ++i) {
auto &wrk = workers[wrkIndex];
wrk->doWork(i).then([&](int seq) {
++resolved;
}, Async::NoExcept);
wrkIndex = (wrkIndex + 1) % Workers;
}
for (;;) {
auto r = resolved.load();
std::cout << r << " promises resolved" << std::endl;
if (r == Ops) break;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "Stopping worker" << std::endl;
for (auto& wrk: workers) {
wrk->stop();
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment