diff --git a/libuavcan/include/uavcan/protocol/dynamic_node_id_allocation_server.hpp b/libuavcan/include/uavcan/protocol/dynamic_node_id_allocation_server.hpp index 80a8c21fb7..2f2e4fa58d 100644 --- a/libuavcan/include/uavcan/protocol/dynamic_node_id_allocation_server.hpp +++ b/libuavcan/include/uavcan/protocol/dynamic_node_id_allocation_server.hpp @@ -70,6 +70,25 @@ public: virtual ~IDynamicNodeIDStorageBackend() { } }; +/** + * This interface allows the application to trace events that happen in the server. + */ +class IDynamicNodeIDAllocationServerEventTracer +{ +public: + /** + * The server invokes this method every time it believes that a noteworthy event has happened. + * The table of event codes can be found in the server sources. + * It is guaranteed that event code values will never change, but new ones can be added in future. This ensures + * full backward compatibility. + * @param event_code Event code, see the sources for the enum with values. + * @param event_argument Value associated with the event; its meaning depends on the event code. + */ + virtual void onEvent(uint16_t event_code, int64_t event_argument) = 0; + + virtual ~IDynamicNodeIDAllocationServerEventTracer() { } +}; + /** * Internals, do not use anything from this namespace directly. */ @@ -83,6 +102,37 @@ using namespace protocol::dynamic_node_id::server; */ typedef StorageType::Type Term; +/** + * @ref IDynamicNodeIDAllocationServerEventTracer. + * Event codes cannot be changed, only new ones can be added. + */ +enum TraceEvent +{ + // Event name Argument + // 0 + TraceError, // error code (may be negated) + TraceLogLastIndexRestored, // recovered last index value + TraceLogAppend, // index of new entry + TraceLogRemove, // new last index value + TraceCurrentTermRestored, // current term + // 5 + TraceCurrentTermUpdate, // current term + TraceVotedForRestored, // value of votedFor + TraceVotedForUpdate, // value of votedFor + TraceDiscoveryBroadcast, // number of known servers + TraceNewServerDiscovered, // node ID of the new server + // 10 + TraceDiscoveryReceived, // node ID of the sender + TraceClusterSizeInited, // cluster size + TraceRaftCoreInited, // update interval in usec + TraceRaftStateSwitch, // 0 - Follower, 1 - Candidate, 2 - Leader + TraceRaftModeSwitch, // 0 - Passive, 1 - Active + // 15 + TraceRaftNewLogEntry, // node ID value + + NumTraceEventCodes +}; + /** * This class extends the storage backend interface with serialization/deserialization functionality. */ @@ -137,6 +187,7 @@ public: private: IDynamicNodeIDStorageBackend& storage_; + IDynamicNodeIDAllocationServerEventTracer& tracer_; Entry entries_[Capacity]; Index last_index_; // Index zero always contains an empty entry @@ -149,8 +200,9 @@ private: int initEmptyLogStorage(); public: - Log(IDynamicNodeIDStorageBackend& storage) + Log(IDynamicNodeIDStorageBackend& storage, IDynamicNodeIDAllocationServerEventTracer& tracer) : storage_(storage) + , tracer_(tracer) , last_index_(0) { } @@ -195,6 +247,7 @@ public: class PersistentState { IDynamicNodeIDStorageBackend& storage_; + IDynamicNodeIDAllocationServerEventTracer& tracer_; Term current_term_; NodeID voted_for_; @@ -204,10 +257,11 @@ class PersistentState static IDynamicNodeIDStorageBackend::String getVotedForKey() { return "voted_for"; } public: - PersistentState(IDynamicNodeIDStorageBackend& storage) + PersistentState(IDynamicNodeIDStorageBackend& storage, IDynamicNodeIDAllocationServerEventTracer& tracer) : storage_(storage) + , tracer_(tracer) , current_term_(0) - , log_(storage) + , log_(storage, tracer) { } int init(); @@ -257,6 +311,7 @@ class ClusterManager : private TimerBase enum { MaxServers = Discovery::FieldTypes::known_nodes::MaxSize }; IDynamicNodeIDStorageBackend& storage_; + IDynamicNodeIDAllocationServerEventTracer& tracer_; const Log& log_; Subscriber discovery_sub_; @@ -293,9 +348,11 @@ public: * @param storage Needed to read the cluster size parameter from the storage * @param log Needed to initialize nextIndex[] values after elections */ - ClusterManager(INode& node, IDynamicNodeIDStorageBackend& storage, const Log& log) + ClusterManager(INode& node, IDynamicNodeIDStorageBackend& storage, const Log& log, + IDynamicNodeIDAllocationServerEventTracer& tracer) : TimerBase(node) , storage_(storage) + , tracer_(tracer) , log_(log) , discovery_sub_(node) , discovery_pub_(node) @@ -348,7 +405,15 @@ public: return false; } + /** + * Number of known servers can only grow, and it never exceeds the cluster size value. + * This number does not include the local server. + */ uint8_t getNumKnownServers() const { return num_known_servers_; } + + /** + * Cluster size and quorum size are constant. + */ uint8_t getClusterSize() const { return cluster_size_; } uint8_t getQuorumSize() const { return static_cast(cluster_size_ / 2U + 1U); } @@ -366,13 +431,13 @@ class RaftCore : private TimerBase ServiceResponseDataStructure&)> AppendEntriesCallback; + typedef MethodBinder&)> + AppendEntriesResponseCallback; + typedef MethodBinder&, ServiceResponseDataStructure&)> RequestVoteCallback; - typedef MethodBinder&)> - AppendEntriesResponseCallback; - typedef MethodBinder&)> RequestVoteResponseCallback; @@ -383,48 +448,95 @@ class RaftCore : private TimerBase ServerStateLeader }; + IDynamicNodeIDAllocationServerEventTracer& tracer_; + + /* + * States + */ PersistentState persistent_state_; - Log::Index commit_index_; ClusterManager cluster_; + Log::Index commit_index_; MonotonicTime last_activity_timestamp_; bool active_mode_; - ServerState server_state_; - ServiceServer append_entries_srv_; - ServiceServer request_vote_srv_; + uint8_t next_server_index_; ///< Next server to query for AE or RV RPC + uint8_t num_votes_received_in_this_campaign_; + /* + * Transport + */ + ServiceServer append_entries_srv_; ServiceClient append_entries_client_; + ServiceServer request_vote_srv_; ServiceClient request_vote_client_; - virtual void handleTimerEvent(const TimerEvent&); + /** + * This constant defines the rate at which internal state updates happen. + * It also defines timeouts for AppendEntries and RequestVote RPCs. + */ + static MonotonicDuration getUpdateInterval() { return MonotonicDuration::fromMSec(50); } + + void trace(TraceEvent event, int64_t argument) { tracer_.onEvent(event, argument); } + + INode& getNode() { return append_entries_srv_.getNode(); } + + void updateFollower(const MonotonicTime& current_time); + void updateCandidate(const MonotonicTime& current_time); + void updateLeader(const MonotonicTime& current_time); + + void switchState(ServerState new_state); + + void handleAppendEntriesRequest(const ReceivedDataStructure& request, + ServiceResponseDataStructure& response); + + void handleAppendEntriesResponse(const ServiceCallResult& result); + + void handleRequestVoteRequest(const ReceivedDataStructure& request, + ServiceResponseDataStructure& response); + + void handleRequestVoteResponse(const ServiceCallResult& result); + + virtual void handleTimerEvent(const TimerEvent& event); public: - RaftCore(INode& node, IDynamicNodeIDStorageBackend& storage) + RaftCore(INode& node, IDynamicNodeIDStorageBackend& storage, IDynamicNodeIDAllocationServerEventTracer& tracer) : TimerBase(node) - , persistent_state_(storage) - , commit_index_(0) // Per Raft paper, commitIndex must be initialized to zero - , cluster_(node, storage, persistent_state_.getLog()) + , tracer_(tracer) + , persistent_state_(storage, tracer) + , cluster_(node, storage, persistent_state_.getLog(), tracer) + , commit_index_(0) // Per Raft paper, commitIndex must be initialized to zero , last_activity_timestamp_(node.getMonotonicTime()) , active_mode_(true) , server_state_(ServerStateFollower) + , next_server_index_(0) + , num_votes_received_in_this_campaign_(0) , append_entries_srv_(node) - , request_vote_srv_(node) , append_entries_client_(node) + , request_vote_srv_(node) , request_vote_client_(node) { } /** * Once started, the logic runs in the background until destructor is called. + * @param cluster_size If set, this value will be used and stored in the persistent storage. If not set, + * value from the persistent storage will be used. If not set and there's no such key + * in the persistent storage, initialization will fail. */ - int init(); + int init(uint8_t cluster_size = ClusterManager::ClusterSizeUnknown); /** - * Inserts one entry into log. This operation may fail, which will not be reported. - * Failures are tolerble because all operations are idempotent. + * Only the leader can call @ref appendLog(). */ - void appendLog(const Entry& entry); + bool isLeader() const { return server_state_ == ServerStateLeader; } + + /** + * Inserts one entry into log. + * Failures are tolerble because all operations are idempotent. + * This method will trigger an assertion failure and return error if the current node is not the leader. + */ + int appendLog(const Entry& entry); /** * This class is used to perform log searches. diff --git a/libuavcan/src/protocol/uc_dynamic_node_id_allocation_server.cpp b/libuavcan/src/protocol/uc_dynamic_node_id_allocation_server.cpp index b3d7ff63c9..56bd9e8773 100644 --- a/libuavcan/src/protocol/uc_dynamic_node_id_allocation_server.cpp +++ b/libuavcan/src/protocol/uc_dynamic_node_id_allocation_server.cpp @@ -286,6 +286,8 @@ int Log::init() last_index_ = Index(value); } + tracer_.onEvent(TraceLogLastIndexRestored, last_index_); + // Restoring log entries - note that index 0 always exists for (Index index = 0; index <= last_index_; index++) { @@ -309,6 +311,8 @@ int Log::append(const Entry& entry) return -ErrLogic; } + tracer_.onEvent(TraceLogAppend, last_index_ + 1U); + // If next operations fail, we'll get a dangling entry, but it's absolutely OK. int res = writeEntryToStorage(Index(last_index_ + 1), entry); if (res < 0) @@ -345,6 +349,8 @@ int Log::removeEntriesWhereIndexGreaterOrEqual(Index index) return -ErrLogic; } + tracer_.onEvent(TraceLogRemove, index - 1U); + MarshallingStorageDecorator io(storage_); uint32_t new_last_index = index - 1U; int res = io.setAndGetBack(getLastIndexKey(), new_last_index); @@ -437,6 +443,8 @@ int PersistentState::init() } } + tracer_.onEvent(TraceCurrentTermRestored, current_term_); + if (current_term_ < last_entry->term) { UAVCAN_TRACE("dynamic_node_id_server_impl::PersistentState", @@ -481,6 +489,8 @@ int PersistentState::init() voted_for_ = NodeID(uint8_t(stored_voted_for)); } + tracer_.onEvent(TraceVotedForRestored, voted_for_.get()); + return 0; } @@ -492,6 +502,8 @@ int PersistentState::setCurrentTerm(const Term term) return -ErrInvalidParam; } + tracer_.onEvent(TraceCurrentTermUpdate, term); + MarshallingStorageDecorator io(storage_); Term tmp = term; @@ -518,6 +530,8 @@ int PersistentState::setVotedFor(const NodeID node_id) return -ErrInvalidParam; } + tracer_.onEvent(TraceVotedForUpdate, node_id.get()); + MarshallingStorageDecorator io(storage_); uint32_t tmp = node_id.get(); @@ -586,6 +600,7 @@ void ClusterManager::addServer(NodeID node_id) UAVCAN_ASSERT((num_known_servers_ + 1) < (MaxServers - 2)); if (!isKnownServer(node_id) && node_id.isUnicast()) { + tracer_.onEvent(TraceNewServerDiscovered, node_id.get()); servers_[num_known_servers_].node_id = node_id; servers_[num_known_servers_].resetIndices(log_); num_known_servers_ = static_cast(num_known_servers_ + 1U); @@ -600,6 +615,8 @@ void ClusterManager::handleTimerEvent(const TimerEvent&) { UAVCAN_ASSERT(num_known_servers_ < cluster_size_); + tracer_.onEvent(TraceDiscoveryBroadcast, num_known_servers_); + /* * Filling the message */ @@ -641,6 +658,8 @@ void ClusterManager::handleTimerEvent(const TimerEvent&) void ClusterManager::handleDiscovery(const ReceivedDataStructure& msg) { + tracer_.onEvent(TraceDiscoveryReceived, msg.getSrcNodeID().get()); + /* * Validating cluster configuration * If there's a case of misconfiguration, the message will be ignored. @@ -730,6 +749,8 @@ int ClusterManager::init(const uint8_t init_cluster_size) } } + tracer_.onEvent(TraceClusterSizeInited, cluster_size_); + UAVCAN_ASSERT(cluster_size_ > 0); UAVCAN_ASSERT(cluster_size_ <= MaxServers); @@ -836,6 +857,158 @@ void ClusterManager::resetAllServerIndices() } } +/* + * RaftCore + */ +void RaftCore::updateFollower(const MonotonicTime& current_time) +{ + (void)current_time; +} + +void RaftCore::updateCandidate(const MonotonicTime& current_time) +{ + (void)current_time; +} + +void RaftCore::updateLeader(const MonotonicTime& current_time) +{ + (void)current_time; +} + +void RaftCore::switchState(ServerState new_state) +{ + if (server_state_ != new_state) + { + trace(TraceRaftStateSwitch, new_state); + } +} + +void RaftCore::handleAppendEntriesRequest(const ReceivedDataStructure& request, + ServiceResponseDataStructure& response) +{ + (void)request; + (void)response; +} + +void RaftCore::handleAppendEntriesResponse(const ServiceCallResult& result) +{ + (void)result; +} + +void RaftCore::handleRequestVoteRequest(const ReceivedDataStructure& request, + ServiceResponseDataStructure& response) +{ + (void)request; + (void)response; +} + +void RaftCore::handleRequestVoteResponse(const ServiceCallResult& result) +{ + (void)result; +} + +void RaftCore::handleTimerEvent(const TimerEvent& event) +{ + switch (server_state_) + { + case ServerStateFollower: + { + updateFollower(event.real_time); + break; + } + case ServerStateCandidate: + { + updateCandidate(event.real_time); + break; + } + case ServerStateLeader: + { + updateLeader(event.real_time); + break; + } + default: + { + UAVCAN_ASSERT(0); + break; + } + } +} + +int RaftCore::init(uint8_t cluster_size) +{ + /* + * Initializing state variables + */ + last_activity_timestamp_ = getNode().getMonotonicTime(); + active_mode_ = true; + server_state_ = ServerStateFollower; + next_server_index_ = 0; + num_votes_received_in_this_campaign_ = 0; + commit_index_ = 0; + + /* + * Initializing internals + */ + int res = persistent_state_.init(); + if (res < 0) + { + return res; + } + + res = cluster_.init(cluster_size); + if (res < 0) + { + return res; + } + + res = append_entries_srv_.start(AppendEntriesCallback(this, &RaftCore::handleAppendEntriesRequest)); + if (res < 0) + { + return res; + } + + res = request_vote_srv_.start(RequestVoteCallback(this, &RaftCore::handleRequestVoteRequest)); + if (res < 0) + { + return res; + } + + res = append_entries_client_.init(); + if (res < 0) + { + return res; + } + append_entries_client_.setRequestTimeout(getUpdateInterval()); + + res = request_vote_client_.init(); + if (res < 0) + { + return res; + } + request_vote_client_.setRequestTimeout(getUpdateInterval()); + + startPeriodic(getUpdateInterval()); + + trace(TraceRaftCoreInited, getUpdateInterval().toUSec()); + + UAVCAN_ASSERT(res >= 0); + return 0; +} + +int RaftCore::appendLog(const Entry& entry) +{ + if (isLeader()) + { + trace(TraceRaftNewLogEntry, entry.node_id); + return persistent_state_.getLog().append(entry); + } + else + { + UAVCAN_ASSERT(0); + return -ErrLogic; + } +} + } // dynamic_node_id_server_impl } diff --git a/libuavcan/test/protocol/dynamic_node_id_allocation_server.cpp b/libuavcan/test/protocol/dynamic_node_id_allocation_server.cpp index 567b386ba1..83e85618ad 100644 --- a/libuavcan/test/protocol/dynamic_node_id_allocation_server.cpp +++ b/libuavcan/test/protocol/dynamic_node_id_allocation_server.cpp @@ -53,6 +53,15 @@ public: }; +class EventTracer : public uavcan::IDynamicNodeIDAllocationServerEventTracer +{ + virtual void onEvent(uavcan::uint16_t event_code, uavcan::int64_t event_argument) + { + std::cout << "Event\t" << event_code << "\t" << event_argument << std::endl; + } +}; + + static const unsigned NumEntriesInStorageWithEmptyLog = 4; // last index + 3 items per log entry @@ -140,10 +149,11 @@ TEST(DynamicNodeIDAllocationServer, MarshallingStorageDecorator) TEST(DynamicNodeIDAllocationServer, LogInitialization) { + EventTracer tracer; // No log data in the storage - initializing empty log { StorageBackend storage; - uavcan::dynamic_node_id_server_impl::Log log(storage); + uavcan::dynamic_node_id_server_impl::Log log(storage, tracer); ASSERT_EQ(0, storage.getNumKeys()); ASSERT_LE(0, log.init()); @@ -157,7 +167,7 @@ TEST(DynamicNodeIDAllocationServer, LogInitialization) // Nonempty storage, one item { StorageBackend storage; - uavcan::dynamic_node_id_server_impl::Log log(storage); + uavcan::dynamic_node_id_server_impl::Log log(storage, tracer); storage.set("log_last_index", "0"); ASSERT_LE(-uavcan::ErrFailure, log.init()); // Expected one entry, none found @@ -174,7 +184,7 @@ TEST(DynamicNodeIDAllocationServer, LogInitialization) // Nonempty storage, broken data { StorageBackend storage; - uavcan::dynamic_node_id_server_impl::Log log(storage); + uavcan::dynamic_node_id_server_impl::Log log(storage, tracer); storage.set("log_last_index", "foobar"); ASSERT_LE(-uavcan::ErrFailure, log.init()); // Bad value @@ -197,7 +207,7 @@ TEST(DynamicNodeIDAllocationServer, LogInitialization) // Nonempty storage, many items { StorageBackend storage; - uavcan::dynamic_node_id_server_impl::Log log(storage); + uavcan::dynamic_node_id_server_impl::Log log(storage, tracer); storage.set("log_last_index", "1"); // 2 items - 0, 1 storage.set("log0_term", "0"); @@ -235,8 +245,9 @@ TEST(DynamicNodeIDAllocationServer, LogInitialization) TEST(DynamicNodeIDAllocationServer, LogAppend) { + EventTracer tracer; StorageBackend storage; - uavcan::dynamic_node_id_server_impl::Log log(storage); + uavcan::dynamic_node_id_server_impl::Log log(storage, tracer); ASSERT_EQ(0, storage.getNumKeys()); ASSERT_LE(0, log.init()); @@ -307,8 +318,9 @@ TEST(DynamicNodeIDAllocationServer, LogAppend) TEST(DynamicNodeIDAllocationServer, LogRemove) { + EventTracer tracer; StorageBackend storage; - uavcan::dynamic_node_id_server_impl::Log log(storage); + uavcan::dynamic_node_id_server_impl::Log log(storage, tracer); /* * Filling the log fully @@ -359,12 +371,13 @@ TEST(DynamicNodeIDAllocationServer, LogRemove) TEST(DynamicNodeIDAllocationServer, PersistentStorageInitialization) { + EventTracer tracer; /* * First initialization */ { StorageBackend storage; - uavcan::dynamic_node_id_server_impl::PersistentState pers(storage); + uavcan::dynamic_node_id_server_impl::PersistentState pers(storage, tracer); ASSERT_EQ(0, storage.getNumKeys()); ASSERT_LE(0, pers.init()); @@ -382,12 +395,12 @@ TEST(DynamicNodeIDAllocationServer, PersistentStorageInitialization) { // This log is used to initialize the storage - uavcan::dynamic_node_id_server_impl::Log log(storage); + uavcan::dynamic_node_id_server_impl::Log log(storage, tracer); ASSERT_LE(0, log.init()); } ASSERT_LE(1, storage.getNumKeys()); - uavcan::dynamic_node_id_server_impl::PersistentState pers(storage); + uavcan::dynamic_node_id_server_impl::PersistentState pers(storage, tracer); ASSERT_LE(0, pers.init()); @@ -404,14 +417,14 @@ TEST(DynamicNodeIDAllocationServer, PersistentStorageInitialization) { // This log is used to initialize the storage - uavcan::dynamic_node_id_server_impl::Log log(storage); + uavcan::dynamic_node_id_server_impl::Log log(storage, tracer); ASSERT_LE(0, log.init()); } ASSERT_LE(1, storage.getNumKeys()); storage.set("current_term", "1"); - uavcan::dynamic_node_id_server_impl::PersistentState pers(storage); + uavcan::dynamic_node_id_server_impl::PersistentState pers(storage, tracer); ASSERT_GT(0, pers.init()); // Fails because current term is not zero @@ -432,7 +445,7 @@ TEST(DynamicNodeIDAllocationServer, PersistentStorageInitialization) { // This log is used to initialize the storage - uavcan::dynamic_node_id_server_impl::Log log(storage); + uavcan::dynamic_node_id_server_impl::Log log(storage, tracer); ASSERT_LE(0, log.init()); uavcan::protocol::dynamic_node_id::server::Entry entry; @@ -443,7 +456,7 @@ TEST(DynamicNodeIDAllocationServer, PersistentStorageInitialization) } ASSERT_LE(4, storage.getNumKeys()); - uavcan::dynamic_node_id_server_impl::PersistentState pers(storage); + uavcan::dynamic_node_id_server_impl::PersistentState pers(storage, tracer); ASSERT_GT(0, pers.init()); // Fails because log is not empty @@ -468,8 +481,9 @@ TEST(DynamicNodeIDAllocationServer, PersistentStorageInitialization) TEST(DynamicNodeIDAllocationServer, PersistentStorage) { + EventTracer tracer; StorageBackend storage; - uavcan::dynamic_node_id_server_impl::PersistentState pers(storage); + uavcan::dynamic_node_id_server_impl::PersistentState pers(storage, tracer); /* * Initializing @@ -549,15 +563,17 @@ TEST(DynamicNodeIDAllocationServer, ClusterManagerInitialization) uavcan::GlobalDataTypeRegistry::instance().reset(); uavcan::DefaultDataTypeRegistrator _reg1; + EventTracer tracer; + /* * Simple initialization */ { StorageBackend storage; - uavcan::dynamic_node_id_server_impl::Log log(storage); + uavcan::dynamic_node_id_server_impl::Log log(storage, tracer); InterlinkedTestNodesWithSysClock nodes; - uavcan::dynamic_node_id_server_impl::ClusterManager mgr(nodes.a, storage, log); + uavcan::dynamic_node_id_server_impl::ClusterManager mgr(nodes.a, storage, log, tracer); // Too big ASSERT_GT(0, mgr.init(MaxClusterSize + 1)); @@ -579,10 +595,10 @@ TEST(DynamicNodeIDAllocationServer, ClusterManagerInitialization) */ { StorageBackend storage; - uavcan::dynamic_node_id_server_impl::Log log(storage); + uavcan::dynamic_node_id_server_impl::Log log(storage, tracer); InterlinkedTestNodesWithSysClock nodes; - uavcan::dynamic_node_id_server_impl::ClusterManager mgr(nodes.a, storage, log); + uavcan::dynamic_node_id_server_impl::ClusterManager mgr(nodes.a, storage, log, tracer); // Not configured ASSERT_GT(0, mgr.init()); @@ -601,11 +617,12 @@ TEST(DynamicNodeIDAllocationServer, ClusterManagerOneServer) uavcan::GlobalDataTypeRegistry::instance().reset(); uavcan::DefaultDataTypeRegistrator _reg1; + EventTracer tracer; StorageBackend storage; - uavcan::dynamic_node_id_server_impl::Log log(storage); + uavcan::dynamic_node_id_server_impl::Log log(storage, tracer); InterlinkedTestNodesWithSysClock nodes; - uavcan::dynamic_node_id_server_impl::ClusterManager mgr(nodes.a, storage, log); + uavcan::dynamic_node_id_server_impl::ClusterManager mgr(nodes.a, storage, log, tracer); /* * Pub and sub @@ -675,11 +692,12 @@ TEST(DynamicNodeIDAllocationServer, ClusterManagerThreeServers) uavcan::GlobalDataTypeRegistry::instance().reset(); uavcan::DefaultDataTypeRegistrator _reg1; + EventTracer tracer; StorageBackend storage; - uavcan::dynamic_node_id_server_impl::Log log(storage); + uavcan::dynamic_node_id_server_impl::Log log(storage, tracer); InterlinkedTestNodesWithSysClock nodes; - uavcan::dynamic_node_id_server_impl::ClusterManager mgr(nodes.a, storage, log); + uavcan::dynamic_node_id_server_impl::ClusterManager mgr(nodes.a, storage, log, tracer); /* * Pub and sub