Add multinode maps and type specific maps.
This lets us do things like remap timing reports per node and handle
them correctly. And also only map the timing reports and not everything
on /aos. This also becomes more interesting when AOS_LOG logs, and we
have a starter which publishes statistics.
Change-Id: I2147e3b722b472d4480c86e7c8acda938aa3d1d2
diff --git a/aos/BUILD b/aos/BUILD
index cf58f4a..aef8a59 100644
--- a/aos/BUILD
+++ b/aos/BUILD
@@ -467,6 +467,7 @@
"testdata/config3.json",
"testdata/expected.json",
"testdata/expected_multinode.json",
+ "testdata/good_multinode.json",
"testdata/invalid_destination_node.json",
"testdata/invalid_nodes.json",
"testdata/invalid_source_node.json",
diff --git a/aos/configuration.cc b/aos/configuration.cc
index f08cf99..112c567 100644
--- a/aos/configuration.cc
+++ b/aos/configuration.cc
@@ -232,17 +232,39 @@
// Maps name for the provided maps. Modifies name.
void HandleMaps(const flatbuffers::Vector<flatbuffers::Offset<aos::Map>> *maps,
- std::string_view *name) {
+ std::string_view *name, std::string_view type,
+ const Node *node) {
// For the same reason we merge configs in reverse order, we want to process
// maps in reverse order. That lets the outer config overwrite channels from
// the inner configs.
for (auto i = maps->rbegin(); i != maps->rend(); ++i) {
- if (i->has_match() && i->match()->has_name() && i->has_rename() &&
- i->rename()->has_name() && i->match()->name()->string_view() == *name) {
- VLOG(1) << "Renamed \"" << *name << "\" to \""
- << i->rename()->name()->string_view() << "\"";
- *name = i->rename()->name()->string_view();
+ if (!i->has_match() || !i->match()->has_name()) {
+ continue;
}
+ if (!i->has_rename() || !i->rename()->has_name()) {
+ continue;
+ }
+
+ // Handle normal maps (now that we know that match and rename are filled
+ // out).
+ if (i->match()->name()->string_view() != *name) {
+ continue;
+ }
+
+ // Handle type specific maps.
+ if (i->match()->has_type() && i->match()->type()->string_view() != type) {
+ continue;
+ }
+
+ if (node != nullptr && i->match()->has_source_node() &&
+ i->match()->source_node()->string_view() !=
+ node->name()->string_view()) {
+ continue;
+ }
+
+ VLOG(1) << "Renamed \"" << *name << "\" to \""
+ << i->rename()->name()->string_view() << "\"";
+ *name = i->rename()->name()->string_view();
}
}
@@ -426,7 +448,8 @@
const Channel *GetChannel(const Configuration *config, std::string_view name,
std::string_view type,
- std::string_view application_name) {
+ std::string_view application_name, const Node *node) {
+ const std::string_view original_name = name;
VLOG(1) << "Looking up { \"name\": \"" << name << "\", \"type\": \"" << type
<< "\" }";
@@ -439,18 +462,20 @@
if (application_iterator != config->applications()->cend() &&
EqualsApplications(*application_iterator, application_name)) {
if (application_iterator->has_maps()) {
- HandleMaps(application_iterator->maps(), &name);
+ HandleMaps(application_iterator->maps(), &name, type, node);
}
}
}
// Now do global maps.
if (config->has_maps()) {
- HandleMaps(config->maps(), &name);
+ HandleMaps(config->maps(), &name, type, node);
}
- VLOG(1) << "Actually looking up { \"name\": \"" << name << "\", \"type\": \""
- << type << "\" }";
+ if (original_name != name) {
+ VLOG(1) << "Remapped to { \"name\": \"" << name << "\", \"type\": \""
+ << type << "\" }";
+ }
// Then look for the channel.
auto channel_iterator =
@@ -461,7 +486,11 @@
// Make sure we actually found it, and it matches.
if (channel_iterator != config->channels()->cend() &&
EqualsChannels(*channel_iterator, std::make_pair(name, type))) {
- VLOG(1) << "Found: " << FlatbufferToJson(*channel_iterator);
+ if (VLOG_IS_ON(2)) {
+ VLOG(2) << "Found: " << FlatbufferToJson(*channel_iterator);
+ } else if (VLOG_IS_ON(1)) {
+ VLOG(1) << "Found: " << CleanedChannelToString(*channel_iterator);
+ }
return *channel_iterator;
} else {
VLOG(1) << "No match for { \"name\": \"" << name << "\", \"type\": \""
@@ -470,6 +499,12 @@
}
}
+std::string CleanedChannelToString(const Channel *channel) {
+ FlatbufferDetachedBuffer<Channel> cleaned_channel = CopyFlatBuffer(channel);
+ cleaned_channel.mutable_message()->clear_schema();
+ return FlatbufferToJson(cleaned_channel);
+}
+
FlatbufferDetachedBuffer<Configuration> MergeConfiguration(
const Flatbuffer<Configuration> &config,
const std::vector<aos::FlatbufferString<reflection::Schema>> &schemas) {
diff --git a/aos/configuration.fbs b/aos/configuration.fbs
index ef25959..36dbacd 100644
--- a/aos/configuration.fbs
+++ b/aos/configuration.fbs
@@ -36,6 +36,7 @@
table Map {
// Channel to match with. If the name in here matches, the name is replaced
// with the name in rename.
+ // Node specific matches are also supported.
match:Channel;
// The channel to merge in.
rename:Channel;
diff --git a/aos/configuration.h b/aos/configuration.h
index 5d4a79b..f2cf499 100644
--- a/aos/configuration.h
+++ b/aos/configuration.h
@@ -37,16 +37,17 @@
//
// If the application name is empty, it is ignored. Maps are processed in
// reverse order, and application specific first.
-const Channel *GetChannel(
- const Configuration *config, const std::string_view name,
- const std::string_view type,
- const std::string_view application_name);
-inline const Channel *GetChannel(
- const Flatbuffer<Configuration> &config,
- const std::string_view name,
- const std::string_view type,
- const std::string_view application_name) {
- return GetChannel(&config.message(), name, type, application_name);
+const Channel *GetChannel(const Configuration *config,
+ const std::string_view name,
+ const std::string_view type,
+ const std::string_view application_name,
+ const Node *node);
+inline const Channel *GetChannel(const Flatbuffer<Configuration> &config,
+ const std::string_view name,
+ const std::string_view type,
+ const std::string_view application_name,
+ const Node *node) {
+ return GetChannel(&config.message(), name, type, application_name, node);
}
// Returns the Node out of the config with the matching name, or nullptr if it
@@ -64,6 +65,9 @@
// provided node.
bool ChannelIsReadableOnNode(const Channel *channel, const Node *node);
+// Prints a channel to json, but without the schema.
+std::string CleanedChannelToString(const Channel *channel);
+
// TODO(austin): GetSchema<T>(const Flatbuffer<Configuration> &config);
// Returns the "root directory" for this run. Under linux, this is the
diff --git a/aos/configuration_test.cc b/aos/configuration_test.cc
index 35b9daf..ff0b570 100644
--- a/aos/configuration_test.cc
+++ b/aos/configuration_test.cc
@@ -22,6 +22,9 @@
// *the* expected location for all working tests.
const char *kExpectedLocation =
"{ \"name\": \"/foo\", \"type\": \".aos.bar\", \"max_size\": 5 }";
+// And for multinode setups
+const char *kExpectedMultinodeLocation =
+ "{ \"name\": \"/foo\", \"type\": \".aos.bar\", \"max_size\": 5, \"source_node\": \"pi1\" }";
// Tests that we can read and merge a configuration.
TEST_F(ConfigurationTest, ConfigMerge) {
@@ -56,7 +59,7 @@
LOG(INFO) << "Read: " << FlatbufferToJson(config, true);
EXPECT_EQ(FlatbufferToJson(GetChannel(config, ".aos.robot_state",
- "aos.RobotState", "app1")),
+ "aos.RobotState", "app1", nullptr)),
"{ \"name\": \".aos.robot_state\", \"type\": \"aos.RobotState\", "
"\"max_size\": 5 }");
}
@@ -78,27 +81,88 @@
ReadConfig("aos/testdata/config1.json");
// Test a basic lookup first.
- EXPECT_EQ(FlatbufferToJson(GetChannel(config, "/foo", ".aos.bar", "app1")),
- kExpectedLocation);
+ EXPECT_EQ(
+ FlatbufferToJson(GetChannel(config, "/foo", ".aos.bar", "app1", nullptr)),
+ kExpectedLocation);
// Test that an invalid name results in nullptr back.
- EXPECT_EQ(GetChannel(config, "/invalid_name", ".aos.bar", "app1"), nullptr);
+ EXPECT_EQ(GetChannel(config, "/invalid_name", ".aos.bar", "app1", nullptr),
+ nullptr);
// Tests that a root map/rename works. And that they get processed from the
// bottom up.
- EXPECT_EQ(
- FlatbufferToJson(GetChannel(config, "/batman", ".aos.bar", "app1")),
- kExpectedLocation);
+ EXPECT_EQ(FlatbufferToJson(
+ GetChannel(config, "/batman", ".aos.bar", "app1", nullptr)),
+ kExpectedLocation);
// And then test that an application specific map/rename works.
- EXPECT_EQ(FlatbufferToJson(GetChannel(config, "/bar", ".aos.bar", "app1")),
- kExpectedLocation);
- EXPECT_EQ(FlatbufferToJson(GetChannel(config, "/baz", ".aos.bar", "app2")),
- kExpectedLocation);
+ EXPECT_EQ(
+ FlatbufferToJson(GetChannel(config, "/bar", ".aos.bar", "app1", nullptr)),
+ kExpectedLocation);
+ EXPECT_EQ(
+ FlatbufferToJson(GetChannel(config, "/baz", ".aos.bar", "app2", nullptr)),
+ kExpectedLocation);
// And then test that an invalid application name gets properly ignored.
- EXPECT_EQ(FlatbufferToJson(GetChannel(config, "/foo", ".aos.bar", "app3")),
- kExpectedLocation);
+ EXPECT_EQ(
+ FlatbufferToJson(GetChannel(config, "/foo", ".aos.bar", "app3", nullptr)),
+ kExpectedLocation);
+}
+
+// Tests that we can lookup a location with node specific maps.
+TEST_F(ConfigurationTest, GetChannelMultinode) {
+ FlatbufferDetachedBuffer<Configuration> config =
+ ReadConfig("aos/testdata/good_multinode.json");
+ const Node *pi1 = GetNode(&config.message(), "pi1");
+ const Node *pi2 = GetNode(&config.message(), "pi2");
+
+ // Test a basic lookup first.
+ EXPECT_EQ(
+ FlatbufferToJson(GetChannel(config, "/foo", ".aos.bar", "app1", pi1)),
+ kExpectedMultinodeLocation);
+ EXPECT_EQ(
+ FlatbufferToJson(GetChannel(config, "/foo", ".aos.bar", "app1", pi2)),
+ kExpectedMultinodeLocation);
+
+ // Tests that a root map/rename works with a node specific map.
+ EXPECT_EQ(FlatbufferToJson(
+ GetChannel(config, "/batman", ".aos.bar", "app1", pi1)),
+ kExpectedMultinodeLocation);
+
+ // Tests that a root map/rename fails with a node specific map for the wrong
+ // node.
+ EXPECT_EQ(GetChannel(config, "/batman", ".aos.bar", "app1", pi2), nullptr);
+
+ // And then test that an application specific map/rename works.
+ EXPECT_EQ(
+ FlatbufferToJson(GetChannel(config, "/batman2", ".aos.bar", "app1", pi1)),
+ kExpectedMultinodeLocation);
+ EXPECT_EQ(
+ FlatbufferToJson(GetChannel(config, "/batman3", ".aos.bar", "app1", pi1)),
+ kExpectedMultinodeLocation);
+
+ // And then that it fails when the node changes.
+ EXPECT_EQ(GetChannel(config, "/batman3", ".aos.bar", "app1", pi2), nullptr);
+}
+
+// Tests that we can lookup a location with type specific maps.
+TEST_F(ConfigurationTest, GetChannelTypedMultinode) {
+ FlatbufferDetachedBuffer<Configuration> config =
+ ReadConfig("aos/testdata/good_multinode.json");
+ const Node *pi1 = GetNode(&config.message(), "pi1");
+
+ // Test a basic lookup first.
+ EXPECT_EQ(
+ FlatbufferToJson(GetChannel(config, "/batman", ".aos.bar", "app1", pi1)),
+ kExpectedMultinodeLocation);
+
+ // Now confirm that a second message on the same name doesn't get remapped.
+ const char *kExpectedBazMultinodeLocation =
+ "{ \"name\": \"/batman\", \"type\": \".aos.baz\", \"max_size\": 5, "
+ "\"source_node\": \"pi1\" }";
+ EXPECT_EQ(
+ FlatbufferToJson(GetChannel(config, "/batman", ".aos.baz", "app1", pi1)),
+ kExpectedBazMultinodeLocation);
}
// Tests that we reject a configuration which has a nodes list, but has channels
diff --git a/aos/events/event_loop.cc b/aos/events/event_loop.cc
index 2c3a215..af5a3e5 100644
--- a/aos/events/event_loop.cc
+++ b/aos/events/event_loop.cc
@@ -355,7 +355,19 @@
// Make a raw sender for the report.
const Channel *channel = configuration::GetChannel(
configuration(), "/aos", timing::Report::GetFullyQualifiedName(),
- name());
+ name(), node());
+
+ // Since we are using a RawSender, validity isn't checked. So check it
+ // ourselves.
+ if (node() != nullptr) {
+ if (!configuration::ChannelIsSendableOnNode(channel, node())) {
+ LOG(FATAL) << "Channel { \"name\": \"/aos"
+ << channel->name()->string_view() << "\", \"type\": \""
+ << channel->type()->string_view()
+ << "\" } is not able to be sent on this node. Check your "
+ "configuration.";
+ }
+ }
CHECK(channel != nullptr) << ": Channel { \"name\": \"/aos\", \"type\": \""
<< timing::Report::GetFullyQualifiedName()
<< "\" } not found in config.";
diff --git a/aos/events/event_loop.h b/aos/events/event_loop.h
index 4cbb07b..bd0c4d4 100644
--- a/aos/events/event_loop.h
+++ b/aos/events/event_loop.h
@@ -312,8 +312,9 @@
// sent to the provided channel.
template <typename T>
Fetcher<T> MakeFetcher(const std::string_view channel_name) {
- const Channel *channel = configuration::GetChannel(
- configuration_, channel_name, T::GetFullyQualifiedName(), name());
+ const Channel *channel =
+ configuration::GetChannel(configuration_, channel_name,
+ T::GetFullyQualifiedName(), name(), node());
CHECK(channel != nullptr)
<< ": Channel { \"name\": \"" << channel_name << "\", \"type\": \""
<< T::GetFullyQualifiedName() << "\" } not found in config.";
@@ -335,8 +336,9 @@
// the provided channel.
template <typename T>
Sender<T> MakeSender(const std::string_view channel_name) {
- const Channel *channel = configuration::GetChannel(
- configuration_, channel_name, T::GetFullyQualifiedName(), name());
+ const Channel *channel =
+ configuration::GetChannel(configuration_, channel_name,
+ T::GetFullyQualifiedName(), name(), node());
CHECK(channel != nullptr)
<< ": Channel { \"name\": \"" << channel_name << "\", \"type\": \""
<< T::GetFullyQualifiedName() << "\" } not found in config.";
diff --git a/aos/events/event_loop_tmpl.h b/aos/events/event_loop_tmpl.h
index 9e5b05d..0b485c6 100644
--- a/aos/events/event_loop_tmpl.h
+++ b/aos/events/event_loop_tmpl.h
@@ -30,7 +30,7 @@
void EventLoop::MakeWatcher(const std::string_view channel_name, Watch &&w) {
using T = typename watch_message_type_trait<Watch>::message_type;
const Channel *channel = configuration::GetChannel(
- configuration_, channel_name, T::GetFullyQualifiedName(), name());
+ configuration_, channel_name, T::GetFullyQualifiedName(), name(), node());
CHECK(channel != nullptr)
<< ": Channel { \"name\": \"" << channel_name << "\", \"type\": \""
diff --git a/aos/testdata/good_multinode.json b/aos/testdata/good_multinode.json
new file mode 100644
index 0000000..94db924
--- /dev/null
+++ b/aos/testdata/good_multinode.json
@@ -0,0 +1,66 @@
+{
+ "channels": [
+ {
+ "name": "/foo",
+ "type": ".aos.bar",
+ "max_size": 5,
+ "source_node": "pi1"
+ },
+ {
+ "name": "/batman",
+ "type": ".aos.baz",
+ "max_size": 5,
+ "source_node": "pi1"
+ }
+ ],
+ "maps": [
+ {
+ "match": {
+ "name": "/batman",
+ "type": ".aos.bar",
+ "source_node": "pi1"
+ },
+ "rename": {
+ "name": "/foo"
+ }
+ }
+ ],
+ "applications": [
+ {
+ "name": "app1",
+ "maps": [
+ {
+ "match": {
+ "name": "/batman2",
+ "source_node": "pi1"
+ },
+ "rename": {
+ "name": "/batman"
+ }
+ },
+ {
+ "match": {
+ "name": "/batman3",
+ "source_node": "pi1"
+ },
+ "rename": {
+ "name": "/foo"
+ }
+ }
+ ]
+ },
+ {
+ "name": "app2"
+ }
+ ],
+ "nodes": [
+ {
+ "name": "pi1",
+ "hostname": "raspberrypi"
+ },
+ {
+ "name": "pi2",
+ "hostname": "raspberrypi2"
+ }
+ ]
+}