Commit f2c9d7e3 authored by Dominik Charousset's avatar Dominik Charousset

Restructure broadcast observables

parent a6583d46
......@@ -18,8 +18,7 @@ concat(Observable x, Observables... xs) {
static_assert(
(std::is_same_v<output_type, output_type_t<Observables>> && ...));
auto hdl = std::move(x).as_observable();
auto ptr = make_counted<merger_impl<output_type>>(hdl.ptr()->ctx());
ptr->concat_mode(true);
auto ptr = make_counted<concat_impl<output_type>>(hdl.ptr()->ctx());
ptr->add(std::move(hdl));
(ptr->add(std::move(xs).as_observable()), ...);
return observable<output_type>{std::move(ptr)};
......
This diff is collapsed.
......@@ -296,10 +296,9 @@ private:
/// a single @ref observable object.
template <class Generator, class... Steps>
class generation final
: public observable_def<
transform_processor_output_type_t<Generator, Steps...>> {
: public observable_def<steps_output_type_t<Generator, Steps...>> {
public:
using output_type = transform_processor_output_type_t<Generator, Steps...>;
using output_type = steps_output_type_t<Generator, Steps...>;
class impl : public observable_impl_base<output_type> {
public:
......
......@@ -477,42 +477,33 @@ private:
// -- utility observer ---------------------------------------------------------
/// Forwards all events to a parent.
template <class T, class Parent, class Token = unit_t>
template <class T, class Parent, class Token>
class forwarder : public ref_counted, public observer_impl<T> {
public:
CAF_INTRUSIVE_PTR_FRIENDS(forwarder)
explicit forwarder(intrusive_ptr<Parent> parent, Token token = Token{})
explicit forwarder(intrusive_ptr<Parent> parent, Token token)
: parent(std::move(parent)), token(std::move(token)) {
// nop
}
void on_complete() override {
if (parent) {
if constexpr (std::is_same_v<Token, unit_t>)
parent->fwd_on_complete(this);
else
parent->fwd_on_complete(this, token);
parent->fwd_on_complete(token);
parent = nullptr;
}
}
void on_error(const error& what) override {
if (parent) {
if constexpr (std::is_same_v<Token, unit_t>)
parent->fwd_on_error(this, what);
else
parent->fwd_on_error(this, token, what);
parent->fwd_on_error(token, what);
parent = nullptr;
}
}
void on_subscribe(subscription new_sub) override {
if (parent) {
if constexpr (std::is_same_v<Token, unit_t>)
parent->fwd_on_subscribe(this, std::move(new_sub));
else
parent->fwd_on_subscribe(this, token, std::move(new_sub));
parent->fwd_on_subscribe(token, std::move(new_sub));
} else {
new_sub.cancel();
}
......@@ -520,10 +511,7 @@ public:
void on_next(span<const T> items) override {
if (parent) {
if constexpr (std::is_same_v<Token, unit_t>)
parent->fwd_on_next(this, items);
else
parent->fwd_on_next(this, token, items);
parent->fwd_on_next(token, items);
}
}
......
......@@ -23,6 +23,18 @@ enum class observer_state {
disposed,
};
/// Returns whether `x` represents a final state, i.e., `completed`, `aborted`
/// or `disposed`.
constexpr bool is_final(observer_state x) noexcept {
return static_cast<int>(x) >= static_cast<int>(observer_state::completed);
}
/// Returns whether `x` represents an active state, i.e., `idle` or
/// `subscribed`.
constexpr bool is_active(observer_state x) noexcept {
return static_cast<int>(x) <= static_cast<int>(observer_state::subscribed);
}
/// @relates sec
CAF_CORE_EXPORT std::string to_string(observer_state);
......
......@@ -16,6 +16,28 @@
namespace caf::flow {
template <class T>
struct identity_step {
using input_type = T;
using output_type = T;
template <class Next, class... Steps>
bool on_next(const input_type& item, Next& next, Steps&... steps) {
return next.on_next(item, steps...);
}
template <class Next, class... Steps>
void on_complete(Next& next, Steps&... steps) {
next.on_complete(steps...);
}
template <class Next, class... Steps>
void on_error(const error& what, Next& next, Steps&... steps) {
next.on_error(what, steps...);
}
};
template <class T>
struct limit_step {
size_t remaining;
......@@ -464,12 +486,15 @@ public:
/// Calls `on_complete` on all observers and drops any pending data.
void close() {
buf_.clear();
if (!err_)
if (!err_) {
for (auto& out : outputs_)
out.sink.on_complete();
else
state_ = observable_state::completed;
} else {
for (auto& out : outputs_)
out.sink.on_error(err_);
state_ = observable_state::aborted;
}
outputs_.clear();
}
......@@ -502,7 +527,6 @@ public:
if (is_active(state_)) {
if (idle()) {
close();
state_ = err_ ? observable_state::aborted : observable_state::completed;
} else {
state_ = observable_state::completing;
}
......@@ -577,4 +601,20 @@ private:
#endif
};
/// Utility for the observables that use one or more steps.
template <class... Steps>
struct steps_output_oracle;
template <class Step>
struct steps_output_oracle<Step> {
using type = typename Step::output_type;
};
template <class Step1, class Step2, class... Steps>
struct steps_output_oracle<Step1, Step2, Steps...>
: steps_output_oracle<Step2, Steps...> {};
template <class... Steps>
using steps_output_type_t = typename steps_output_oracle<Steps...>::type;
} // namespace caf::flow
......@@ -166,7 +166,7 @@ private:
}
template <size_t I>
void fwd_on_subscribe(ref_counted*, zipper_index<I> index, subscription sub) {
void fwd_on_subscribe(zipper_index<I> index, subscription sub) {
if (!term_.finalized()) {
auto& in = at(index);
if (!in.sub) {
......@@ -182,7 +182,7 @@ private:
}
template <size_t I>
void fwd_on_complete(ref_counted*, zipper_index<I> index) {
void fwd_on_complete(zipper_index<I> index) {
if (!broken_) {
broken_ = true;
if (at(index).buf.empty())
......@@ -192,7 +192,7 @@ private:
}
template <size_t I>
void fwd_on_error(ref_counted*, zipper_index<I> index, const error& what) {
void fwd_on_error(zipper_index<I> index, const error& what) {
if (!term_.err())
term_.err(what);
if (!broken_) {
......@@ -204,7 +204,7 @@ private:
}
template <size_t I, class T>
void fwd_on_next(ref_counted*, zipper_index<I> index, span<const T> items) {
void fwd_on_next(zipper_index<I> index, span<const T> items) {
if (!term_.finalized()) {
auto& buf = at(index).buf;
buf.insert(buf.end(), items.begin(), items.end());
......
......@@ -27,7 +27,7 @@ BEGIN_FIXTURE_SCOPE(fixture)
SCENARIO("a broadcaster pushes items to all subscribers at the same time") {
GIVEN("a broadcaster with one source and three sinks") {
auto uut = make_counted<flow::broadcaster_impl<int>>(ctx.get());
auto uut = flow::make_broadcaster_impl<int>(ctx.get());
auto src = flow::make_passive_observable<int>(ctx.get());
auto snk1 = flow::make_passive_observer<int>();
auto snk2 = flow::make_passive_observer<int>();
......@@ -71,7 +71,7 @@ SCENARIO("a broadcaster pushes items to all subscribers at the same time") {
SCENARIO("a broadcaster emits values before propagating completion") {
GIVEN("a broadcaster with one source and three sinks") {
auto uut = make_counted<flow::broadcaster_impl<int>>(ctx.get());
auto uut = flow::make_broadcaster_impl<int>(ctx.get());
auto src = flow::make_passive_observable<int>(ctx.get());
auto snk1 = flow::make_passive_observer<int>();
auto snk2 = flow::make_passive_observer<int>();
......@@ -114,7 +114,7 @@ SCENARIO("a broadcaster emits values before propagating completion") {
SCENARIO("a broadcaster emits values before propagating errors") {
GIVEN("a broadcaster with one source and three sinks") {
auto uut = make_counted<flow::broadcaster_impl<int>>(ctx.get());
auto uut = flow::make_broadcaster_impl<int>(ctx.get());
auto src = flow::make_passive_observable<int>(ctx.get());
auto snk1 = flow::make_passive_observer<int>();
auto snk2 = flow::make_passive_observer<int>();
......
......@@ -17,12 +17,108 @@ namespace {
struct fixture : test_coordinator_fixture<> {
flow::scoped_coordinator_ptr ctx = flow::make_scoped_coordinator();
template <class... Ts>
std::vector<int> ls(Ts... xs) {
return std::vector<int>{xs...};
}
};
} // namespace
BEGIN_FIXTURE_SCOPE(fixture)
SCENARIO("concatenate processes inputs sequentially") {
GIVEN("a concatenation with no inputs and shutdown-on-last-complete ON") {
auto uut = make_counted<flow::concat_impl<int>>(ctx.get());
WHEN("subscribing to the concatenation") {
THEN("the concatenation immediately closes") {
auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer());
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::aborted);
CHECK_EQ(snk->err, sec::disposed);
CHECK(snk->buf.empty());
}
}
}
GIVEN("a concatenation with no inputs and shutdown-on-last-complete OFF") {
auto uut = make_counted<flow::concat_impl<int>>(ctx.get());
uut->shutdown_on_last_complete(false);
WHEN("subscribing to the concatenation") {
THEN("the concatenation accepts the subscription and does nothing else") {
auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer());
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK(snk->buf.empty());
}
}
}
GIVEN("a concatenation with one input that completes") {
WHEN("subscribing and requesting before the first push") {
auto uut = make_counted<flow::concat_impl<int>>(ctx.get());
auto src = flow::make_passive_observable<int>(ctx.get());
uut->add(src->as_observable());
ctx->run();
auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer());
ctx->run();
THEN("the concatenation forwards all items from the source") {
MESSAGE("the observer enters the state subscribed");
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls());
MESSAGE("when requesting data, no data is received yet");
snk->sub.request(2);
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls());
MESSAGE("after pushing, the observer immediately receives them");
src->push(1, 2);
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls(1, 2));
MESSAGE("when requesting more data, the observer gets the remainder");
snk->sub.request(20);
ctx->run();
src->push(3, 4, 5);
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5));
MESSAGE("the concatenation closes if the source closes");
src->complete();
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5));
}
}
}
GIVEN("a concatenation with one input that aborts after some items") {
WHEN("subscribing to the concatenation") {
auto uut = make_counted<flow::concat_impl<int>>(ctx.get());
auto src = flow::make_passive_observable<int>(ctx.get());
uut->add(src->as_observable());
ctx->run();
auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer());
ctx->run();
THEN("the concatenation forwards all items until the error") {
MESSAGE("after the source pushed five items, it emits an error");
snk->sub.request(20);
ctx->run();
src->push(1, 2, 3, 4, 5);
ctx->run();
src->abort(make_error(sec::runtime_error));
ctx->run();
MESSAGE("the observer obtains the and then the error");
CHECK_EQ(snk->state, flow::observer_state::aborted);
CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5));
CHECK_EQ(snk->err, make_error(sec::runtime_error));
}
}
}
}
SCENARIO("concat operators combine inputs") {
GIVEN("two observables") {
WHEN("merging them to a single publisher via concat") {
......
......@@ -17,12 +17,144 @@ namespace {
struct fixture : test_coordinator_fixture<> {
flow::scoped_coordinator_ptr ctx = flow::make_scoped_coordinator();
template <class... Ts>
std::vector<int> ls(Ts... xs) {
return std::vector<int>{xs...};
}
};
} // namespace
BEGIN_FIXTURE_SCOPE(fixture)
SCENARIO("mergers round-robin over their inputs") {
GIVEN("a merger with no inputs and shutdown-on-last-complete ON") {
auto uut = make_counted<flow::merger_impl<int>>(ctx.get());
WHEN("subscribing to the merger") {
THEN("the merger immediately closes") {
auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer());
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::aborted);
CHECK_EQ(snk->err, sec::disposed);
CHECK(snk->buf.empty());
}
}
}
GIVEN("a merger with no inputs and shutdown-on-last-complete OFF") {
auto uut = make_counted<flow::merger_impl<int>>(ctx.get());
uut->shutdown_on_last_complete(false);
WHEN("subscribing to the merger") {
THEN("the merger accepts the subscription and does nothing else") {
auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer());
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK(snk->buf.empty());
}
}
}
GIVEN("a round-robin merger with one input that completes") {
WHEN("subscribing to the merger and requesting before the first push") {
auto uut = make_counted<flow::merger_impl<int>>(ctx.get());
auto src = flow::make_passive_observable<int>(ctx.get());
uut->add(src->as_observable());
ctx->run();
auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer());
ctx->run();
THEN("the merger forwards all items from the source") {
MESSAGE("the observer enters the state subscribed");
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls());
MESSAGE("when requesting data, no data is received yet");
snk->sub.request(2);
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls());
MESSAGE("after pushing, the observer immediately receives them");
src->push(1, 2, 3, 4, 5);
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls(1, 2));
MESSAGE("when requesting more data, the observer gets the remainder");
snk->sub.request(20);
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5));
MESSAGE("the merger closes if the source closes");
src->complete();
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5));
}
}
AND_WHEN("subscribing to the merger pushing before the first request") {
auto uut = make_counted<flow::merger_impl<int>>(ctx.get());
auto src = flow::make_passive_observable<int>(ctx.get());
uut->add(src->as_observable());
ctx->run();
auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer());
ctx->run();
THEN("the merger forwards all items from the source") {
MESSAGE("the observer enters the state subscribed");
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls());
MESSAGE("after pushing, the observer receives nothing yet");
src->push(1, 2, 3, 4, 5);
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls());
MESSAGE("the observer get the first items immediately when requesting");
snk->sub.request(2);
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls(1, 2));
MESSAGE("when requesting more data, the observer gets the remainder");
snk->sub.request(20);
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5));
MESSAGE("the merger closes if the source closes");
src->complete();
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5));
}
}
}
GIVEN("a round-robin merger with one input that aborts after some items") {
WHEN("subscribing to the merger") {
auto uut = make_counted<flow::merger_impl<int>>(ctx.get());
auto src = flow::make_passive_observable<int>(ctx.get());
uut->add(src->as_observable());
ctx->run();
auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer());
ctx->run();
THEN("the merger forwards all items from the source until the error") {
MESSAGE("after the source pushed five items, it emits an error");
src->push(1, 2, 3, 4, 5);
ctx->run();
src->abort(make_error(sec::runtime_error));
ctx->run();
MESSAGE("when requesting, the observer still obtains the items first");
snk->sub.request(2);
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls(1, 2));
snk->sub.request(20);
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::aborted);
CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5));
CHECK_EQ(snk->err, make_error(sec::runtime_error));
}
}
}
}
SCENARIO("merge operators combine inputs") {
GIVEN("two observables") {
WHEN("merging them to a single publisher") {
......
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