Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion phlex/core/declared_fold.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ namespace phlex::detail {
counter_for(index_hash_for_counter).increment(index->layer_hash());

emit_and_evict_if_done(fold_index);
}}
}},
graph_{g}
{
if constexpr (num_inputs > 1ull) {
make_edge(join_, fold_);
Expand Down Expand Up @@ -206,6 +207,8 @@ namespace phlex::detail {
results_;
std::atomic<std::size_t> calls_;
std::atomic<std::size_t> product_count_;
tbb::flow::graph& graph() const override { return graph_; }
std::reference_wrapper<tbb::flow::graph> graph_;
};
}

Expand Down
5 changes: 4 additions & 1 deletion phlex/core/declared_observer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ namespace phlex::detail {
call(ft, messages, std::make_index_sequence<num_inputs>{});
++calls_;
return {};
}}
}},
graph_{g}
{
if constexpr (num_inputs > 1ull) {
make_edge(join_, observer_);
Expand Down Expand Up @@ -107,6 +108,8 @@ namespace phlex::detail {
join_or_none_t<num_inputs> join_;
tbb::flow::function_node<messages_t<num_inputs>> observer_;
std::atomic<std::size_t> calls_;
tbb::flow::graph& graph() const override { return graph_; }
std::reference_wrapper<tbb::flow::graph> graph_;
};
}

Expand Down
5 changes: 4 additions & 1 deletion phlex/core/declared_predicate.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ namespace phlex::detail {
bool const rc = call(ft, messages, std::make_index_sequence<num_inputs>{});
++calls_;
return {message_id, rc};
}}
}},
graph_{g}
{
if constexpr (num_inputs > 1ull) {
make_edge(join_, predicate_);
Expand Down Expand Up @@ -115,6 +116,8 @@ namespace phlex::detail {
join_or_none_t<num_inputs> join_;
tbb::flow::function_node<messages_t<num_inputs>, predicate_result> predicate_;
std::atomic<std::size_t> calls_;
tbb::flow::graph& graph() const override { return graph_; }
std::reference_wrapper<tbb::flow::graph> graph_;
};

}
Expand Down
6 changes: 5 additions & 1 deletion phlex/core/declared_transform.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ namespace phlex::detail {
store->index(), name(), std::move(new_products));

std::get<0>(output).try_put({.store = std::move(new_store), .id = message_id});
}}
}},
graph_{g}
{
if constexpr (num_inputs > 1ull) {
make_edge(join_, transform_);
Expand Down Expand Up @@ -149,6 +150,9 @@ namespace phlex::detail {
tbb::flow::multifunction_node<messages_t<num_inputs>, message_tuple<1u>> transform_;
std::atomic<std::size_t> calls_;
tbb::concurrent_unordered_map<std::size_t, std::atomic<std::size_t>> product_count_;

tbb::flow::graph& graph() const override { return graph_; }
std::reference_wrapper<tbb::flow::graph> graph_;
};

}
Expand Down
5 changes: 4 additions & 1 deletion phlex/core/declared_unfold.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ namespace phlex::detail {
std::get<2>(outputs).try_put({.index = store->index(),
.layer_hash = gen.child_layer_hash(),
.count = gen.child_count()});
}}
}},
graph_{g}
{
if constexpr (num_inputs > 1ull) {
make_edge(join_, unfold_);
Expand Down Expand Up @@ -200,6 +201,8 @@ namespace phlex::detail {
std::atomic<std::size_t> msg_counter_{}; // Is this sufficient? Probably not.
std::atomic<std::size_t> calls_{};
std::atomic<std::size_t> product_count_{};
tbb::flow::graph& graph() const override { return graph_; }
std::reference_wrapper<tbb::flow::graph> graph_;
};
}

Expand Down
8 changes: 5 additions & 3 deletions phlex/core/make_computational_edges.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,16 @@ namespace phlex::detail {

for (auto const& query : node->input()) {
auto* receiver_port = collector ? collector : &node->port(query);
auto producer = producers.find_producer(query, node->name());
if (not producer) {
auto producer_ports = producers.find_producers(query, node->name());
if (producer_ports.empty()) {
// Is there a way to detect mis-specified product dependencies?
result[node_name].push_back({query, receiver_port});
Comment on lines +151 to 153

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back when only wrong-layer producers match

Because find_producers() is not layer-aware, a producer on a different layer still makes this branch treat the input as satisfied. In a graph with, for example, an event transform producing suffix number and a job provider for the same suffix/type, a job selector reaches this path, the provider lookup is skipped, and the new layer checker drops the event messages, leaving the consumer with no input instead of using the matching provider or reporting the product as unresolved.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex Strictly, those products are also distinguishable by stage name so a stage name should be provided. Our semantics are that where no stage name is provided that ambiguity is resolved by preferring the product in the current stage.

Nevertheless, can you suggest a way to detect this ambiguity where it actually exists?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex couldn't complete this request. Try again later.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex Partially resolved by bailing early if a stage name other than "CURRENT" is requested.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex couldn't complete this request. Try again later.

continue;
}

make_edge(*producer->output_port, *receiver_port);
for (auto* producer : producer_ports) {
make_edge(*producer->output_port, *receiver_port);
Comment on lines +157 to +158

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve layer checks for filtered consumers

When a consumer has experimental_when, receiver_port is the filter's collector, so this loop connects every matching producer directly to the filter and never calls node->port(query). The new layer-checking node is only created inside products_consumer::port(), while filter forwards to consumer.ports() (the raw downstream ports), so a filtered node whose selector asks for job can still receive matching event producer messages whenever the predicate accepts those message ids.

Useful? React with 👍 / 👎.

}
}
}
return result;
Expand Down
68 changes: 58 additions & 10 deletions phlex/core/producer_catalog.cpp
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
#include "phlex/core/producer_catalog.hpp"
#include "phlex/utilities/bulleted_list.hpp"
#include "phlex/utilities/hashing.hpp"

#include "fmt/format.h"
#include "fmt/ranges.h"
#include "spdlog/spdlog.h"
#include <ranges>

namespace phlex::detail {
producer_catalog::named_output_port const* producer_catalog::find_producer(
std::vector<producer_catalog::named_output_port const*> producer_catalog::find_producers(
product_selector const& query, phlex::experimental::algorithm_name const& consumer_name) const
{
// Will need an update when we have a way to set the current stage name
if (query.stage.has_value() && query.stage.value() != "CURRENT"_idq) {
spdlog::debug(
"{} requires a stage other than the current one. Assuming it comes from a provider.",
query.to_string());
return {};
}
if (producers_.empty()) {
spdlog::debug("No producers found. Skipping and assuming {} comes from a provider.",
query.to_string());
return nullptr;
return {};
}
// Now the only way b == e is if we have a suffix and nothing creates matching products
auto [b, e] = query.suffix.has_value() ? producers_.equal_range(*query.suffix)
Expand All @@ -22,9 +30,18 @@ namespace phlex::detail {
spdlog::debug(
"Failed to find an algorithm that creates {} products. Assuming it comes from a provider",
query.suffix.value_or("*"_id));
return nullptr;
return {};
}
std::map<std::string, named_output_port const*> candidates;
std::vector<named_output_port const*> candidates;

// Don't really want to copy these fields so we'll use hashes and store a pointer to one copy
std::map<std::uint64_t, experimental::identifier const*> suffixes;
if (query.suffix.has_value()) {
suffixes.emplace(query.suffix->hash(), &*query.suffix);
}
std::map<std::uint64_t, experimental::algorithm_name const*> creators;
std::map<std::uint64_t, type_id const*> types;

for (auto const& [key, producer] : std::ranges::subrange{b, e}) {
// Prevent self-edges
if (producer.node == consumer_name) {
Expand Down Expand Up @@ -59,7 +76,14 @@ namespace phlex::detail {
query.type.exact_name(),
producer.type.exact_name());
}
candidates.emplace(producer.node.to_string(), &producer);
candidates.push_back(&producer);
if (!query.suffix.has_value()) {
suffixes.emplace(key.hash(), &key);
}
creators.emplace(
phlex::detail::hash(producer.node.plugin().hash(), producer.node.algorithm().hash()),
&producer.node);
types.emplace(hash_value(producer.type), &producer.type);
}
} else {
spdlog::debug("Creator name mismatch between ({}) and {}",
Expand All @@ -72,17 +96,41 @@ namespace phlex::detail {
spdlog::debug(
"Cannot identify product matching the query {}. Assuming it comes from a provider.",
query.to_string());
return nullptr;
}

if (candidates.size() > 1ull) {
std::string msg = fmt::format("More than one candidate matches the query {}: \n{}\n",
static auto const port_to_node =
[](named_output_port const* p) -> experimental::algorithm_name const& { return p->node; };
static auto const deref_view =
std::views::transform([]<typename T>(T const* p) -> T const& { return *p; });
std::string msg = fmt::format("More than one candidate matches the query {}: \n{}",
query.to_string(),
bulleted_list(std::views::keys(candidates),
bulleted_list(std::views::transform(candidates, port_to_node),
/*indent=*/1));
throw std::runtime_error(msg);
if (suffixes.size() == 1 && creators.size() == 1 && types.size() == 1) {
spdlog::info(msg);
spdlog::info("This is permitted -- layers may differ");
return candidates;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep same-layer duplicate producers ambiguous

This branch permits multiple producers solely because suffix, creator, and type are identical, but the catalog has not checked that the candidates actually differ by layer. In configurations like a transform and a fold registered with the same name and suffix that both emit on the selector's layer, all candidates are returned and connected; the downstream layer checker accepts both, causing duplicate executions instead of the previous ambiguity error.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex Can you suggest how I might detect this situation?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex couldn't complete this request. Try again later.

}
spdlog::error(msg);

if (suffixes.size() > 1) {
spdlog::error("Not permitted -- distinguishable by suffix {}",
suffixes | std::views::values | deref_view);
}
if (creators.size() > 1) {
spdlog::error("Not permitted -- distinguishable by creator {}",
creators | std::views::values |
std::views::transform(
[](experimental::algorithm_name const* p) { return p->to_string(); }));
}
if (types.size() > 1) {
spdlog::error("Not permitted -- distinguishable by type {}",
types | std::views::values | deref_view);
}
throw std::runtime_error("Multiple products in candidate set -- see errors");
}

return candidates.begin()->second;
return candidates;
}
}
2 changes: 1 addition & 1 deletion phlex/core/producer_catalog.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ namespace phlex::detail {
type_id type;
};

named_output_port const* find_producer(
std::vector<named_output_port const*> find_producers(
product_selector const& query,
phlex::experimental::algorithm_name const& consumer_name) const;
auto values() const { return producers_ | std::views::values; }
Expand Down
16 changes: 15 additions & 1 deletion phlex/core/products_consumer.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "phlex/core/products_consumer.hpp"
#include <spdlog/spdlog.h>

namespace {
std::vector<phlex::experimental::identifier> layers_from(phlex::product_selectors const& queries)
Expand Down Expand Up @@ -29,7 +30,20 @@ namespace phlex::detail {

tbb::flow::receiver<message>& products_consumer::port(product_selector const& input_product)
{
return port_for(input_product);
// Everything has a layer for now, so everything needs this
auto& next = port_for(input_product);

auto& layer_check = layer_checkers_.emplace_back(std::make_unique<layer_check_node_t>(
graph(),
tbb::flow::unlimited,
[&layer = static_cast<experimental::identifier const&>(input_product.layer)](
message const& msg, auto& output) {
if (msg.store->layer_name() == layer) {
std::get<0>(output).try_put(msg);
}
}));
make_edge(tbb::flow::output_port<0>(*layer_check), next);
return *layer_check;
}

product_selectors const& products_consumer::input() const noexcept { return input_products_; }
Expand Down
5 changes: 4 additions & 1 deletion phlex/core/products_consumer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

namespace phlex::detail {
class PHLEX_CORE_EXPORT products_consumer : public consumer {
using layer_check_node_t = tbb::flow::multifunction_node<message, message_tuple<1UZ>>;

public:
products_consumer(phlex::experimental::algorithm_name name,
std::vector<std::string> predicates,
Expand All @@ -44,9 +46,10 @@ namespace phlex::detail {

private:
virtual tbb::flow::receiver<message>& port_for(product_selector const& input_product) = 0;

virtual tbb::flow::graph& graph() const = 0;
product_selectors input_products_;
std::vector<phlex::experimental::identifier> layers_;
std::vector<std::unique_ptr<layer_check_node_t>> layer_checkers_;
};
}

Expand Down
19 changes: 19 additions & 0 deletions test/product_selecting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,23 @@ TEST_CASE("Querying products in different ways", "[graph]")
g.execute();
CHECK(g.execution_count("creator_and_layer_after_transform") == num_events);
}

SECTION("Products from this job, using layer only")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the section above (i.e., SECTION("Layer alone, distinguished by type")) not already cover this case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the section above specifies the layer only because layer is a mandatory field. The product lookup is done by type.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But with your changes, wouldn't the lookup then also be done by layer as well?

I'm trying to determine how the construction you've added is different than what's already there...and it's not clear to me how it is.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The selector requires layer to be event and type to be string (LIST char). Before and after my change, the product is uniquely identified by the type alone (i.e. there's only one node producing string products).

In the new test, the layer name is required to disambiguate, since there are two duplicate_temperature nodes producing temperature products of type double in the current stage.

{
// create a product to be found
g.fold(
"duplicate_temperature",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should trigger an exception before executing the graph: there's already a transform algorithm named "duplicate_temperature" earlier in the file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It certainly doesn't appear to.

[](std::atomic<double>& summary, double temp) { summary += temp; },
concurrency::unlimited,
"job")
.input_family(product_selector{.creator = "input", .layer = "event"})
.output_product_suffixes("temperature");

// Find it
g.transform("layer_only", [](double const& d) { return d; })
.input_family(product_selector{.layer = "job"})
.output_product_suffixes("job_temp");
g.execute();
CHECK(g.execution_count("layer_only") == 1); // 1 job
}
}
Loading