Commit f2c9d7e3 authored by Dominik Charousset's avatar Dominik Charousset

Restructure broadcast observables

parent a6583d46
...@@ -18,8 +18,7 @@ concat(Observable x, Observables... xs) { ...@@ -18,8 +18,7 @@ concat(Observable x, Observables... xs) {
static_assert( static_assert(
(std::is_same_v<output_type, output_type_t<Observables>> && ...)); (std::is_same_v<output_type, output_type_t<Observables>> && ...));
auto hdl = std::move(x).as_observable(); auto hdl = std::move(x).as_observable();
auto ptr = make_counted<merger_impl<output_type>>(hdl.ptr()->ctx()); auto ptr = make_counted<concat_impl<output_type>>(hdl.ptr()->ctx());
ptr->concat_mode(true);
ptr->add(std::move(hdl)); ptr->add(std::move(hdl));
(ptr->add(std::move(xs).as_observable()), ...); (ptr->add(std::move(xs).as_observable()), ...);
return observable<output_type>{std::move(ptr)}; return observable<output_type>{std::move(ptr)};
......
This diff is collapsed.
...@@ -296,10 +296,9 @@ private: ...@@ -296,10 +296,9 @@ private:
/// a single @ref observable object. /// a single @ref observable object.
template <class Generator, class... Steps> template <class Generator, class... Steps>
class generation final class generation final
: public observable_def< : public observable_def<steps_output_type_t<Generator, Steps...>> {
transform_processor_output_type_t<Generator, Steps...>> {
public: 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> { class impl : public observable_impl_base<output_type> {
public: public:
......
...@@ -477,42 +477,33 @@ private: ...@@ -477,42 +477,33 @@ private:
// -- utility observer --------------------------------------------------------- // -- utility observer ---------------------------------------------------------
/// Forwards all events to a parent. /// 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> { class forwarder : public ref_counted, public observer_impl<T> {
public: public:
CAF_INTRUSIVE_PTR_FRIENDS(forwarder) 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)) { : parent(std::move(parent)), token(std::move(token)) {
// nop // nop
} }
void on_complete() override { void on_complete() override {
if (parent) { if (parent) {
if constexpr (std::is_same_v<Token, unit_t>) parent->fwd_on_complete(token);
parent->fwd_on_complete(this);
else
parent->fwd_on_complete(this, token);
parent = nullptr; parent = nullptr;
} }
} }
void on_error(const error& what) override { void on_error(const error& what) override {
if (parent) { if (parent) {
if constexpr (std::is_same_v<Token, unit_t>) parent->fwd_on_error(token, what);
parent->fwd_on_error(this, what);
else
parent->fwd_on_error(this, token, what);
parent = nullptr; parent = nullptr;
} }
} }
void on_subscribe(subscription new_sub) override { void on_subscribe(subscription new_sub) override {
if (parent) { if (parent) {
if constexpr (std::is_same_v<Token, unit_t>) parent->fwd_on_subscribe(token, std::move(new_sub));
parent->fwd_on_subscribe(this, std::move(new_sub));
else
parent->fwd_on_subscribe(this, token, std::move(new_sub));
} else { } else {
new_sub.cancel(); new_sub.cancel();
} }
...@@ -520,10 +511,7 @@ public: ...@@ -520,10 +511,7 @@ public:
void on_next(span<const T> items) override { void on_next(span<const T> items) override {
if (parent) { if (parent) {
if constexpr (std::is_same_v<Token, unit_t>) parent->fwd_on_next(token, items);
parent->fwd_on_next(this, items);
else
parent->fwd_on_next(this, token, items);
} }
} }
......
...@@ -23,6 +23,18 @@ enum class observer_state { ...@@ -23,6 +23,18 @@ enum class observer_state {
disposed, 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 /// @relates sec
CAF_CORE_EXPORT std::string to_string(observer_state); CAF_CORE_EXPORT std::string to_string(observer_state);
......
...@@ -16,6 +16,28 @@ ...@@ -16,6 +16,28 @@
namespace caf::flow { 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> template <class T>
struct limit_step { struct limit_step {
size_t remaining; size_t remaining;
...@@ -464,12 +486,15 @@ public: ...@@ -464,12 +486,15 @@ public:
/// Calls `on_complete` on all observers and drops any pending data. /// Calls `on_complete` on all observers and drops any pending data.
void close() { void close() {
buf_.clear(); buf_.clear();
if (!err_) if (!err_) {
for (auto& out : outputs_) for (auto& out : outputs_)
out.sink.on_complete(); out.sink.on_complete();
else state_ = observable_state::completed;
} else {
for (auto& out : outputs_) for (auto& out : outputs_)
out.sink.on_error(err_); out.sink.on_error(err_);
state_ = observable_state::aborted;
}
outputs_.clear(); outputs_.clear();
} }
...@@ -502,7 +527,6 @@ public: ...@@ -502,7 +527,6 @@ public:
if (is_active(state_)) { if (is_active(state_)) {
if (idle()) { if (idle()) {
close(); close();
state_ = err_ ? observable_state::aborted : observable_state::completed;
} else { } else {
state_ = observable_state::completing; state_ = observable_state::completing;
} }
...@@ -577,4 +601,20 @@ private: ...@@ -577,4 +601,20 @@ private:
#endif #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 } // namespace caf::flow
...@@ -166,7 +166,7 @@ private: ...@@ -166,7 +166,7 @@ private:
} }
template <size_t I> 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()) { if (!term_.finalized()) {
auto& in = at(index); auto& in = at(index);
if (!in.sub) { if (!in.sub) {
...@@ -182,7 +182,7 @@ private: ...@@ -182,7 +182,7 @@ private:
} }
template <size_t I> template <size_t I>
void fwd_on_complete(ref_counted*, zipper_index<I> index) { void fwd_on_complete(zipper_index<I> index) {
if (!broken_) { if (!broken_) {
broken_ = true; broken_ = true;
if (at(index).buf.empty()) if (at(index).buf.empty())
...@@ -192,7 +192,7 @@ private: ...@@ -192,7 +192,7 @@ private:
} }
template <size_t I> 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()) if (!term_.err())
term_.err(what); term_.err(what);
if (!broken_) { if (!broken_) {
...@@ -204,7 +204,7 @@ private: ...@@ -204,7 +204,7 @@ private:
} }
template <size_t I, class T> 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()) { if (!term_.finalized()) {
auto& buf = at(index).buf; auto& buf = at(index).buf;
buf.insert(buf.end(), items.begin(), items.end()); buf.insert(buf.end(), items.begin(), items.end());
......
...@@ -27,7 +27,7 @@ BEGIN_FIXTURE_SCOPE(fixture) ...@@ -27,7 +27,7 @@ BEGIN_FIXTURE_SCOPE(fixture)
SCENARIO("a broadcaster pushes items to all subscribers at the same time") { SCENARIO("a broadcaster pushes items to all subscribers at the same time") {
GIVEN("a broadcaster with one source and three sinks") { 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 src = flow::make_passive_observable<int>(ctx.get());
auto snk1 = flow::make_passive_observer<int>(); auto snk1 = flow::make_passive_observer<int>();
auto snk2 = 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") { ...@@ -71,7 +71,7 @@ SCENARIO("a broadcaster pushes items to all subscribers at the same time") {
SCENARIO("a broadcaster emits values before propagating completion") { SCENARIO("a broadcaster emits values before propagating completion") {
GIVEN("a broadcaster with one source and three sinks") { 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 src = flow::make_passive_observable<int>(ctx.get());
auto snk1 = flow::make_passive_observer<int>(); auto snk1 = flow::make_passive_observer<int>();
auto snk2 = flow::make_passive_observer<int>(); auto snk2 = flow::make_passive_observer<int>();
...@@ -114,7 +114,7 @@ SCENARIO("a broadcaster emits values before propagating completion") { ...@@ -114,7 +114,7 @@ SCENARIO("a broadcaster emits values before propagating completion") {
SCENARIO("a broadcaster emits values before propagating errors") { SCENARIO("a broadcaster emits values before propagating errors") {
GIVEN("a broadcaster with one source and three sinks") { 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 src = flow::make_passive_observable<int>(ctx.get());
auto snk1 = flow::make_passive_observer<int>(); auto snk1 = flow::make_passive_observer<int>();
auto snk2 = flow::make_passive_observer<int>(); auto snk2 = flow::make_passive_observer<int>();
......
...@@ -17,12 +17,108 @@ namespace { ...@@ -17,12 +17,108 @@ namespace {
struct fixture : test_coordinator_fixture<> { struct fixture : test_coordinator_fixture<> {
flow::scoped_coordinator_ptr ctx = flow::make_scoped_coordinator(); flow::scoped_coordinator_ptr ctx = flow::make_scoped_coordinator();
template <class... Ts>
std::vector<int> ls(Ts... xs) {
return std::vector<int>{xs...};
}
}; };
} // namespace } // namespace
BEGIN_FIXTURE_SCOPE(fixture) 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") { SCENARIO("concat operators combine inputs") {
GIVEN("two observables") { GIVEN("two observables") {
WHEN("merging them to a single publisher via concat") { WHEN("merging them to a single publisher via concat") {
......
...@@ -17,12 +17,144 @@ namespace { ...@@ -17,12 +17,144 @@ namespace {
struct fixture : test_coordinator_fixture<> { struct fixture : test_coordinator_fixture<> {
flow::scoped_coordinator_ptr ctx = flow::make_scoped_coordinator(); flow::scoped_coordinator_ptr ctx = flow::make_scoped_coordinator();
template <class... Ts>
std::vector<int> ls(Ts... xs) {
return std::vector<int>{xs...};
}
}; };
} // namespace } // namespace
BEGIN_FIXTURE_SCOPE(fixture) 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") { SCENARIO("merge operators combine inputs") {
GIVEN("two observables") { GIVEN("two observables") {
WHEN("merging them to a single publisher") { 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