Commit d9639b60 authored by Dominik Charousset's avatar Dominik Charousset

Merge pull request #1382

parents e6f6dab9 e7a0c077
......@@ -47,6 +47,9 @@ is based on [Keep a Changelog](https://keepachangelog.com).
could end up in a long-running read loop. To avoid potentially starving other
actors or activities, scheduled actors now limit the amount of actions that
may run in one iteration (#1364).
- Destroying a consumer or producer resource before opening it lead to a stall
of the consumer / producer. The buffer now keeps track of whether `close` or
`abort` were called prior to consumers or producers attaching.
### Deprecated
......
......@@ -45,10 +45,8 @@ public:
struct flags {
/// Stores whether `close` has been called.
bool closed : 1;
/// Stores whether the buffer had a consumer at some point.
bool had_consumer : 1;
/// Stores whether the buffer had a producer at some point.
bool had_producer : 1;
/// Stores whether `cancel` has been called.
bool canceled : 1;
};
spsc_buffer(uint32_t capacity, uint32_t min_pull_size)
......@@ -125,20 +123,14 @@ public:
/// Closes the buffer by request of the producer.
void close() {
lock_type guard{mtx_};
if (producer_) {
flags_.closed = true;
producer_ = nullptr;
if (buf_.empty() && consumer_)
consumer_->on_producer_wakeup();
}
abort(error{});
}
/// Closes the buffer by request of the producer and signals an error to the
/// consumer.
void abort(error reason) {
lock_type guard{mtx_};
if (producer_) {
if (!flags_.closed) {
flags_.closed = true;
err_ = std::move(reason);
producer_ = nullptr;
......@@ -150,7 +142,8 @@ public:
/// Closes the buffer by request of the consumer.
void cancel() {
lock_type guard{mtx_};
if (consumer_) {
if (!flags_.canceled) {
flags_.canceled = true;
consumer_ = nullptr;
if (producer_)
producer_->on_consumer_cancel();
......@@ -162,12 +155,11 @@ public:
CAF_ASSERT(consumer != nullptr);
lock_type guard{mtx_};
if (consumer_)
CAF_RAISE_ERROR("SPSC buffer already has a consumer");
CAF_RAISE_ERROR(std::logic_error, "SPSC buffer already has a consumer");
consumer_ = std::move(consumer);
flags_.had_consumer = true;
if (producer_)
ready();
else if (flags_.had_producer)
else if (flags_.closed)
consumer_->on_producer_wakeup();
}
......@@ -176,12 +168,11 @@ public:
CAF_ASSERT(producer != nullptr);
lock_type guard{mtx_};
if (producer_)
CAF_RAISE_ERROR("SPSC buffer already has a producer");
CAF_RAISE_ERROR(std::logic_error, "SPSC buffer already has a producer");
producer_ = std::move(producer);
flags_.had_producer = true;
if (consumer_)
ready();
else if (flags_.had_consumer)
else if (flags_.canceled)
producer_->on_consumer_cancel();
}
......@@ -347,7 +338,7 @@ struct resource_ctrl : ref_counted {
if (buf) {
if constexpr (IsProducer) {
auto err = make_error(sec::disposed,
"producer_resource destroyed without opening it");
"destroyed producer_resource without opening it");
buf->abort(err);
} else {
buf->cancel();
......@@ -356,13 +347,12 @@ struct resource_ctrl : ref_counted {
}
buffer_ptr try_open() {
auto res = buffer_ptr{};
std::unique_lock guard{mtx};
if (buf) {
auto res = buffer_ptr{};
res.swap(buf);
return res;
}
return nullptr;
return res;
}
mutable std::mutex mtx;
......@@ -431,6 +421,20 @@ public:
return ctrl_ != nullptr;
}
bool operator!() const noexcept {
return ctrl_ == nullptr;
}
friend bool operator==(const consumer_resource& lhs,
const consumer_resource& rhs) {
return lhs.ctrl_ == rhs.ctrl_;
}
friend bool operator!=(const consumer_resource& lhs,
const consumer_resource& rhs) {
return lhs.ctrl_ != rhs.ctrl_;
}
private:
intrusive_ptr<resource_ctrl<T, false>> ctrl_;
};
......@@ -485,10 +489,30 @@ public:
buf->close();
}
/// Calls `try_open` and on success immediately calls `abort` on the buffer.
void abort(error reason) {
if (auto buf = try_open())
buf->abort(std::move(reason));
}
explicit operator bool() const noexcept {
return ctrl_ != nullptr;
}
bool operator!() const noexcept {
return ctrl_ == nullptr;
}
friend bool operator==(const producer_resource& lhs,
const producer_resource& rhs) {
return lhs.ctrl_ == rhs.ctrl_;
}
friend bool operator!=(const producer_resource& lhs,
const producer_resource& rhs) {
return lhs.ctrl_ != rhs.ctrl_;
}
private:
intrusive_ptr<resource_ctrl<T, true>> ctrl_;
};
......
......@@ -477,8 +477,8 @@ disposable observable<T>::subscribe(async::producer_resource<T> resource) {
using adapter_type = buffer_writer_impl<buffer_type>;
if (auto buf = resource.try_open()) {
CAF_LOG_DEBUG("subscribe producer resource to flow");
auto adapter = make_counted<adapter_type>(pimpl_->ctx(), buf);
buf->set_producer(adapter);
auto adapter = make_counted<adapter_type>(pimpl_->ctx());
adapter->init(buf);
auto obs = adapter->as_observer();
auto sub = subscribe(std::move(obs));
pimpl_->ctx()->watch(sub);
......@@ -754,8 +754,8 @@ async::consumer_resource<T>
observable<T>::to_resource(size_t buffer_size, size_t min_request_size) {
using buffer_type = async::spsc_buffer<T>;
auto buf = make_counted<buffer_type>(buffer_size, min_request_size);
auto up = make_counted<buffer_writer_impl<buffer_type>>(pimpl_->ctx(), buf);
buf->set_producer(up);
auto up = make_counted<buffer_writer_impl<buffer_type>>(pimpl_->ctx());
up->init(buf);
subscribe(up->as_observer());
return async::consumer_resource<T>{std::move(buf)};
}
......
......@@ -287,10 +287,8 @@ public:
// -- constructors, destructors, and assignment operators --------------------
buffer_writer_impl(coordinator* ctx, buffer_ptr buf)
: ctx_(ctx), buf_(std::move(buf)) {
buffer_writer_impl(coordinator* ctx) : ctx_(ctx) {
CAF_ASSERT(ctx_ != nullptr);
CAF_ASSERT(buf_ != nullptr);
}
~buffer_writer_impl() {
......@@ -298,6 +296,15 @@ public:
buf_->close();
}
void init(buffer_ptr buf) {
// This step is a bit subtle. Basically, buf->set_producer might throw, in
// which case we must not set buf_ to avoid closing a buffer that we don't
// actually own.
CAF_ASSERT(buf != nullptr);
buf->set_producer(this);
buf_ = std::move(buf);
}
// -- intrusive_ptr interface ------------------------------------------------
friend void intrusive_ptr_add_ref(const buffer_writer_impl* ptr) noexcept {
......
......@@ -35,10 +35,6 @@ public:
}
~from_resource_sub() {
// The buffer points back to this object as consumer, so this cannot be
// destroyed unless we have called buf_->cancel(). All code paths that do
// call cancel() on the buffer also must set the variable to `nullptr`.
CAF_ASSERT(buf_ == nullptr);
ctx_->deref_execution_context();
}
......
......@@ -108,6 +108,44 @@ struct dummy_observer {
BEGIN_FIXTURE_SCOPE(test_coordinator_fixture<>)
TEST_CASE("resources may be copied") {
auto [rd, wr] = async::make_spsc_buffer_resource<int>(6, 2);
// Test copy constructor.
async::consumer_resource<int> rd2{rd};
CHECK_EQ(rd, rd2);
async::producer_resource<int> wr2{wr};
CHECK_EQ(wr, wr2);
// Test copy-assignment.
async::consumer_resource<int> rd3;
CHECK_NE(rd2, rd3);
rd3 = rd2;
CHECK_EQ(rd2, rd3);
async::producer_resource<int> wr3;
CHECK_NE(wr2, wr3);
wr3 = wr2;
CHECK_EQ(wr2, wr3);
}
TEST_CASE("resources may be moved") {
auto [rd, wr] = async::make_spsc_buffer_resource<int>(6, 2);
CHECK(rd);
CHECK(wr);
// Test move constructor.
async::consumer_resource<int> rd2{std::move(rd)};
CHECK(!rd);
CHECK(rd2);
async::producer_resource<int> wr2{std::move(wr)};
CHECK(!wr);
CHECK(wr2);
// Test move-assignment.
async::consumer_resource<int> rd3{std::move(rd2)};
CHECK(!rd2);
CHECK(rd3);
async::producer_resource<int> wr3{std::move(wr2)};
CHECK(!wr2);
CHECK(wr3);
}
SCENARIO("SPSC buffers may go past their capacity") {
GIVEN("an SPSC buffer with consumer and produer") {
auto prod = make_counted<dummy_producer>();
......@@ -126,7 +164,7 @@ SCENARIO("SPSC buffers may go past their capacity") {
buf->push(2);
CHECK_EQ(cons->producer_wakeups, 1u);
THEN("excess items are stored but do not trigger demand when consumed") {
std::vector<int> tmp{3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14};
auto tmp = std::vector<int>{3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14};
buf->push(make_span(tmp));
prod->demand = 0;
CHECK_EQ(cons->producer_wakeups, 1u);
......@@ -153,8 +191,42 @@ SCENARIO("SPSC buffers may go past their capacity") {
}
}
SCENARIO("the prioritize_errors policy skips processing of pending items") {
GIVEN("an SPSC buffer with consumer and produer") {
WHEN("pushing into the buffer and then aborting") {
THEN("pulling items with prioritize_errors skips remaining items") {
auto prod = make_counted<dummy_producer>();
auto cons = make_counted<dummy_consumer>();
auto buf = make_counted<async::spsc_buffer<int>>(10, 2);
auto tmp = std::vector<int>{1, 2, 3, 4, 5};
buf->set_producer(prod);
buf->push(make_span(tmp));
buf->set_consumer(cons);
CHECK_EQ(cons->producer_wakeups, 1u);
MESSAGE("consume one element");
{
dummy_observer obs;
auto [ok, consumed] = buf->pull(async::prioritize_errors, 1, obs);
CHECK_EQ(ok, true);
CHECK_EQ(consumed, 1u);
CHECK_EQ(obs.consumed, 1u);
}
MESSAGE("set an error and try consuming remaining elements");
{
buf->abort(sec::runtime_error);
dummy_observer obs;
auto [ok, consumed] = buf->pull(async::prioritize_errors, 1, obs);
CHECK_EQ(ok, false);
CHECK_EQ(consumed, 0u);
CHECK_EQ(obs.err, sec::runtime_error);
}
}
}
}
}
SCENARIO("SPSC buffers moves data between actors") {
GIVEN("a SPSC buffer resource") {
GIVEN("an SPSC buffer resource") {
WHEN("opening the resource from two actors") {
THEN("data travels through the SPSC buffer") {
using actor_t = event_based_actor;
......@@ -180,4 +252,311 @@ SCENARIO("SPSC buffers moves data between actors") {
}
}
SCENARIO("SPSC buffers appear empty when only one actor is connected") {
GIVEN("an SPSC buffer resource") {
WHEN("destroying the write end before adding a subscriber") {
THEN("no data arrives through the SPSC buffer") {
using actor_t = event_based_actor;
auto outputs = std::vector<int>{};
auto finalized = false;
{
auto [rd, wr] = async::make_spsc_buffer_resource<int>(6, 2);
sys.spawn([rd{rd}, &outputs, &finalized](actor_t* snk) {
snk
->make_observable() //
.from_resource(rd)
.do_finally([&finalized] { finalized = true; })
.for_each([&outputs](int x) { outputs.emplace_back(x); });
});
}
// At scope exit, `wr` gets destroyed, closing the buffer.
run();
CHECK(finalized);
CHECK(outputs.empty());
}
}
WHEN("destroying the write end after adding a subscriber") {
THEN("no data arrives through the SPSC buffer") {
using actor_t = event_based_actor;
auto outputs = std::vector<int>{};
auto finalized = false;
{
auto [rd, wr] = async::make_spsc_buffer_resource<int>(6, 2);
sys.spawn([rd{rd}, &outputs, &finalized](actor_t* snk) {
snk
->make_observable() //
.from_resource(rd)
.do_finally([&finalized] { finalized = true; })
.for_each([&outputs](int x) { outputs.emplace_back(x); });
});
// Only difference to before: have the actor create the observable
// from `rd` handle before destroying `wr`.
run();
}
// At scope exit, `wr` gets destroyed, closing the buffer.
run();
CHECK(finalized);
CHECK(outputs.empty());
}
}
WHEN("aborting the write end") {
THEN("the observer receives on_error") {
using actor_t = event_based_actor;
auto outputs = std::vector<int>{};
auto on_error_called = false;
auto [rd, wr] = async::make_spsc_buffer_resource<int>(6, 2);
sys.spawn([rd{rd}, &outputs, &on_error_called](actor_t* snk) {
snk
->make_observable() //
.from_resource(rd)
.do_on_error([&on_error_called](const error& err) {
on_error_called = true;
CHECK_EQ(err, sec::runtime_error);
})
.for_each([&outputs](int x) { outputs.emplace_back(x); });
});
wr.abort(sec::runtime_error);
wr.abort(sec::runtime_error); // Calling twice must have no side effect.
run();
CHECK(on_error_called);
CHECK(outputs.empty());
}
}
WHEN("closing the write end") {
THEN("the observer receives on_complete") {
using actor_t = event_based_actor;
auto outputs = std::vector<int>{};
auto on_complete_called = false;
auto [rd, wr] = async::make_spsc_buffer_resource<int>(6, 2);
sys.spawn([rd{rd}, &outputs, &on_complete_called](actor_t* snk) {
snk
->make_observable() //
.from_resource(rd)
.do_on_complete([&on_complete_called] { //
on_complete_called = true;
})
.for_each([&outputs](int x) { outputs.emplace_back(x); });
});
wr.close();
wr.close(); // Calling twice must have no side effect.
run();
CHECK(on_complete_called);
CHECK(outputs.empty());
}
}
}
}
SCENARIO("SPSC buffers drop data when discarding the read end") {
GIVEN("an SPSC buffer resource") {
WHEN("destroying the read end before adding a publisher") {
THEN("the flow of the writing actor gets canceled") {
using actor_t = event_based_actor;
auto outputs = std::vector<int>{};
{
auto [rd, wr] = async::make_spsc_buffer_resource<int>(6, 2);
sys.spawn([wr{wr}](actor_t* src) {
src
->make_observable() //
.iota(1)
.subscribe(wr);
});
}
// At scope exit, `rd` gets destroyed, closing the buffer.
run();
CHECK(outputs.empty());
}
}
WHEN("destroying the read end before adding a publisher") {
THEN("the flow of the writing actor gets canceled") {
using actor_t = event_based_actor;
auto outputs = std::vector<int>{};
{
auto [rd, wr] = async::make_spsc_buffer_resource<int>(6, 2);
sys.spawn([wr{wr}](actor_t* src) {
src
->make_observable() //
.iota(1)
.subscribe(wr);
});
// Only difference to before: have the actor add an observer that
// writes to `wr` before destroying `rd`.
run();
}
// At scope exit, `rd` gets destroyed, closing the buffer.
run();
CHECK(outputs.empty());
}
}
WHEN("canceling the read end before adding a publisher") {
THEN("the flow of the writing actor gets canceled") {
using actor_t = event_based_actor;
auto outputs = std::vector<int>{};
auto [rd, wr] = async::make_spsc_buffer_resource<int>(6, 2);
sys.spawn([wr{wr}](actor_t* src) {
src
->make_observable() //
.iota(1)
.subscribe(wr);
});
rd.cancel();
rd.cancel(); // Calling twice must have no side effect.
run();
CHECK(outputs.empty());
}
}
}
}
SCENARIO("resources are invalid after calling try_open") {
GIVEN("a producer resource") {
WHEN("opening it twice") {
THEN("the second try_open fails") {
auto [rd, wr] = async::make_spsc_buffer_resource<int>(6, 2);
CHECK(rd);
CHECK_NE(rd.try_open(), nullptr);
CHECK(!rd);
CHECK_EQ(rd.try_open(), nullptr);
}
}
}
}
SCENARIO("producer resources may be subscribed to flows only once") {
GIVEN("a producer resource") {
WHEN("subscribing it to a flow twice") {
THEN("the second attempt results in a canceled subscription") {
using actor_t = event_based_actor;
auto outputs = std::vector<int>{};
auto [rd, wr] = async::make_spsc_buffer_resource<int>(6, 2);
auto prod1 = sys.spawn([wr{wr}](actor_t* src) {
src
->make_observable() //
.iota(1)
.subscribe(wr);
});
self->monitor(prod1);
run();
auto prod2 = sys.spawn([wr{wr}](actor_t* src) {
src
->make_observable() //
.iota(1)
.subscribe(wr);
});
self->monitor(prod2);
run();
expect((down_msg), to(self).with(down_msg{prod2.address(), error{}}));
CHECK(self->mailbox().empty());
}
}
}
}
SCENARIO("consumer resources may be converted to flows only once") {
GIVEN("a consumer resource") {
WHEN("making an observable from the resource") {
THEN("the second attempt results in an empty observable") {
using actor_t = event_based_actor;
auto outputs = std::vector<int>{};
auto [rd, wr] = async::make_spsc_buffer_resource<int>(6, 2);
auto snk1 = sys.spawn([rd{rd}, &outputs](actor_t* snk) {
snk
->make_observable() //
.from_resource(rd)
.for_each([&outputs](int x) { outputs.emplace_back(x); });
});
self->monitor(snk1);
run();
auto snk2 = sys.spawn([rd{rd}, &outputs](actor_t* snk) {
snk
->make_observable() //
.from_resource(rd)
.for_each([&outputs](int x) { outputs.emplace_back(x); });
});
self->monitor(snk2);
run();
expect((down_msg), to(self).with(down_msg{snk2.address(), error{}}));
CHECK(self->mailbox().empty());
CHECK(outputs.empty());
}
}
}
}
#ifdef CAF_ENABLE_EXCEPTIONS
// Note: this basically checks that buffer protects against misuse and is not
// how users should do things.
SCENARIO("SPSC buffers reject multiple producers") {
GIVEN("an SPSC buffer resource") {
WHEN("attaching a second producer") {
THEN("the buffer immediately calls on_consumer_cancel on it") {
// Note: two resources should never point to the same buffer.
using actor_t = event_based_actor;
using buffer_type = async::spsc_buffer<int>;
auto buf = make_counted<buffer_type>(20, 5);
auto rd = async::consumer_resource<int>{buf};
auto wr1 = async::producer_resource<int>{buf};
auto wr2 = async::producer_resource<int>{buf};
auto prod1 = sys.spawn([wr1](actor_t* src) {
src->make_observable().iota(1).subscribe(wr1);
});
self->monitor(prod1);
run();
auto prod2 = sys.spawn([wr2](actor_t* src) {
src->make_observable().iota(1).subscribe(wr2);
});
self->monitor(prod2);
run();
// prod2 dies immediately to the exception.
expect((down_msg),
to(self).with(down_msg{prod2.address(), sec::runtime_error}));
CHECK(self->mailbox().empty());
}
}
}
}
// Note: this basically checks that buffer protects against misuse and is not
// how users should do things.
SCENARIO("SPSC buffers reject multiple consumers") {
GIVEN("an SPSC buffer resource") {
WHEN("attaching a second consumer") {
THEN("the buffer throws an exception") {
// Note: two resources should never point to the same buffer.
using actor_t = event_based_actor;
using buffer_type = async::spsc_buffer<int>;
auto buf = make_counted<buffer_type>(20, 5);
auto rd1 = async::consumer_resource<int>{buf};
auto rd2 = async::consumer_resource<int>{buf};
auto wr = async::producer_resource<int>{buf};
auto outputs = std::vector<int>{};
auto snk1 = sys.spawn([rd1, &outputs](actor_t* snk) {
snk
->make_observable() //
.from_resource(rd1)
.for_each([&outputs](int x) { outputs.emplace_back(x); });
});
self->monitor(snk1);
run();
auto snk2 = sys.spawn([rd2, &outputs](actor_t* snk) {
snk
->make_observable() //
.from_resource(rd2)
.for_each([&outputs](int x) { outputs.emplace_back(x); });
});
self->monitor(snk2);
run();
// snk2 dies immediately to the exception.
expect((down_msg),
to(self).with(down_msg{snk2.address(), sec::runtime_error}));
CHECK(self->mailbox().empty());
CHECK(outputs.empty());
}
}
}
}
#endif // CAF_ENABLE_EXCEPTIONS
END_FIXTURE_SCOPE()
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