Skip to content

Conversation

@stuqdog
Copy link
Member

@stuqdog stuqdog commented Jan 31, 2024

High Level Changes

  • Remove init method for registering resources pre-main. Instead, call a Registry::initialize() method to handle all registration.
  • Simplify ResourceRegistration2, move it into a register_resource method.
  • Move all resources onto ResourceRegistration2, removing the need to create unique {ResourceType}Registration classes.
  • Ensure that resource servers are properly registered when creating a RobotService such that resources acquired from that robot service can actually make gRPC calls.
    • In order to accomplish this, I moved management of ResourceServers onto the Server class. I considered creating a top level Viam class (which would own both the servers and the initialize() method currently on Registry), but decided against this because it would then need to be passed around to various methods and thus would complicate user-facing APIs.
    • this in turn led to a refactor of the ModuleService. Previously it was managing services by itself but this ended up being pretty duplicative of the new server-based ownership module, so I moved it over to just looking to the Server to manage everything.

Model mybase_model("viam", "base", "mybase");

// // CR erodkin: add note about how we've reduced the comment here.
// Make sure to explicity register resources with custom APIs.
Copy link
Member Author

Choose a reason for hiding this comment

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

Removed the "must be done in main and not in resource implementation" bit of this comment because the idea of doing registration pre-main doesn't really make sense anymore.


private:
// CR erodkin: this is a bit of a hacky workaround, leave a comment about it
API api() const override;
Copy link
Member Author

Choose a reason for hiding this comment

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

We added the ability to look up API for a ResourceServer so that we can register them properly within the server. Unfortunately the RobotService is technically a ResourceServer! It doesn't really make sense for it to have an API that people can lookup so I just threw it in private to try and hide it. Curious if folks have thoughts on a better approach.

Copy link
Member

Choose a reason for hiding this comment

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

You can't really hide it because you can always call it via the base class pointer. Why doesn't the RobotService have an API? Shouldn't it just be the API of Robot?

Copy link
Member Author

Choose a reason for hiding this comment

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

There is no Robot base class, and there is no Robot API. Certainly not in the SDK, but (to my knowledge) not anywhere in the codebase. The type of an API (either component or service) breaks down with robot, because component or service is a resource type on the particular robot. It doesn't really make sense in my head to think of the API of a robot.

Comment on lines -31 to -32
// TODO: make `register_service` take one of our types as an arg rather than a
// grpc service type, and convert under the hood
Copy link
Member Author

Choose a reason for hiding this comment

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

I tried to do this but ran into weird issues. We can't just go to registering a ResourceServer type unless we make it clear under the hood that it's a grpc::Service. But, we can't have the ResourceServer base type inherit from grpc::Service because, e.g., MotorServer needs to inherit from MotorService, and then we get an ambiguous type hierarchy inference when trying to register. We could solve this by adding a to_grpc_service method that does appropriate typecasting or something like that, but this is pretty hacky and I don't love it. Add to this the fact that users (even those implementing new services) should never have to touch this method and it seemed not worth looking into this todo anymore.

Happy to revert if folks disagree.

Copy link
Member

Choose a reason for hiding this comment

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

I think it's fine to just take in the grpc::Service*.

}
}

void ModuleService::add_api_from_registry_inlock_(API api, const std::lock_guard<std::mutex>&) {
Copy link
Member Author

Choose a reason for hiding this comment

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

This was adding managed resource servers/resources from the registry to the module_. But, since all resource servers are now owned/maintained by the Server, this is no longer necessary.

std::unordered_map<std::string, std::shared_ptr<ModelRegistration>> registry;
for (auto& resource : resources_) {
registry.emplace(resource.first, resource.second);
const google::protobuf::ServiceDescriptor* Registry::get_service_descriptor_(
Copy link
Member Author

Choose a reason for hiding this comment

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

Just moving some logic from the header to the implementation file.

return service_descriptor_;
}

void register_resources() {
Copy link
Member Author

Choose a reason for hiding this comment

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

A bit of a gotcha for Viam developers, we'll now need to add new resources here. It should be hard to miss; client_to_mock_pipeline handles Registry initialization and so tests will fail if resources aren't registered.

@stuqdog stuqdog marked this pull request as ready for review January 31, 2024 19:35
@stuqdog stuqdog requested a review from a team as a code owner January 31, 2024 19:35
@stuqdog stuqdog requested review from njooma and removed request for a team January 31, 2024 19:35
Comment on lines 14 to 18
Registry::initialize();
for (const auto& rr : Registry::registered_resources()) {
auto new_manager = std::make_shared<ResourceManager>();
auto server = rr.second->create_resource_server(new_manager, *this);
managed_servers_.emplace(server->api(), std::move(server));
Copy link
Member Author

@stuqdog stuqdog Jan 31, 2024

Choose a reason for hiding this comment

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

Register all known resources when creating the server, to ensure proper gRPC reflection.

I don't like having this logic (Registry initializing, creating of managed resource servers) happening inside the Server constructor. But, it does have the nice property of creating only a single point where all this is handled, and not requiring users to call initialize themselves (which could be a bit of a gotcha).

I tried moving this logic into the Server::start() call where I think it makes a bit more sense, but this created a problem where it was impossible to add a managed resource until the server had already started. This made it impossible, e.g., for the RobotService to make sure that all its managed resources were taken care of at construction.

Another consideration I had was removing static from all Registry methods, having the Server class own a single instance of a registry, and then we can handle all important registry initialization in the Registry constructor. This is nice and clean. But, it created a dependency loop (server.hpp would need to include registry.hpp, and registry.hpp needs to include server.hpp for the register_resource call. Moving the server.register_service call out of registry.hpp and into the server constructor (and thus solving this dependency loop) puts us in the case described above with ambiguous inheritance to grpc::service. We could add a to_grpc_service() method on all ResourceServers to account for this but 1) this creates extra boilerplate, and 2) making Registry non-static imposes other limitations (e.g., we can no longer register resources until a Server has been created). This would involve a fair amount of refactoring and testing and I'm not convinced would be a strict upgrade.

Given that this is relatively hidden from users (they should never have to deal with registration at this level, or deal with the look of this implementation), I thought the usefulness properties and lack of user-facing issues outweighed the slight code-smell that comes from it. Happy to revisit if others disagree.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah I'm really feeling the lack of clarity around what the root of initialization and ownership is in the SDK. I don't think we are going to get there in this review.

Copy link
Member Author

Choose a reason for hiding this comment

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

😞 yeah that is fair. I suspect a second PR that focuses broadly on this specific question will be important in the near future.

Copy link
Member

@acmorrow acmorrow left a comment

Choose a reason for hiding this comment

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

@stuqdog - I'm starting into this review today. I've just taken a quick skim through so far. Is it fair to say that as far as the changes under src/viam/sdk/{components,services}/* that it is more or less identical changes for each of base, board, etc.? If so that will save me some time reviewing as I can take a close look at the changes for one component and one service and basically mark-as-read the others. It will be easier to see the real changes that way.

@stuqdog
Copy link
Member Author

stuqdog commented Feb 1, 2024

@stuqdog - I'm starting into this review today. I've just taken a quick skim through so far. Is it fair to say that as far as the changes under src/viam/sdk/{components,services}/* that it is more or less identical changes for each of base, board, etc.? If so that will save me some time reviewing as I can take a close look at the changes for one component and one service and basically mark-as-read the others. It will be easier to see the real changes that way.

@acmorrow hi yes, that is a fair assessment!

@stuqdog stuqdog marked this pull request as draft February 2, 2024 16:07
@stuqdog stuqdog marked this pull request as ready for review February 2, 2024 16:29

target_sources(viamsdk
PRIVATE
# TODO(RSDK-1742): we have to put `registry` up top here because we need the
Copy link
Member

Choose a reason for hiding this comment

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

👍

}

void Registry::initialize() {
if (initialized_) {
Copy link
Member

Choose a reason for hiding this comment

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

This needs some sort of thread safety.

return manager_;
};

std::shared_ptr<ResourceManager>& ResourceServer::resource_manager() {
Copy link
Member

Choose a reason for hiding this comment

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

Return type seems dangerous here, as it would allow you to write:

some_resource_server.resource_manager() = nullptr;

Copy link
Member Author

Choose a reason for hiding this comment

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

Ahh yeah, good catch. Upon reflection I'm not sure why I added this at all. We already had a const version, and since the inner ResourceManager isn't const we can still call the setter methods. Will remove.


private:
// CR erodkin: this is a bit of a hacky workaround, leave a comment about it
API api() const override;
Copy link
Member

Choose a reason for hiding this comment

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

You can't really hide it because you can always call it via the base class pointer. Why doesn't the RobotService have an API? Shouldn't it just be the API of Robot?


void register_resources() {
// Register all components
Registry::register_resource<BaseClient, BaseServer, component::base::v1::BaseService>(
Copy link
Member

Choose a reason for hiding this comment

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

Whenever I see code like this, I start thinking about how we can template meta-program it away over a type list of some sort.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I tried to figure out some way to do that with boost::mpl::list but got pretty lost in the weeds and walked away with the sense that boost::mpl wasn't able to do what I needed. I'm very very far from an expert on C++ meta-programming however, and more than happy to be pointed in a better direction or otherwise proven wrong!

// of that type from the server.
Registry::register_resource(API::get<Gizmo>(), Gizmo::resource_registration());
Registry::register_resource(API::get<Summation>(), Summation::resource_registration());
Registry::register_resource<GizmoClient, GizmoServer, GizmoService>(API::get<Gizmo>());
Copy link
Member

Choose a reason for hiding this comment

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

There's four types here now, where there more or less used to be one. Now this knows of the client type, the server type, the GRPC service type, and the "C++ SDK Interface" type (for lack of a better term).

But:

  • The server type always inherits from the ::Server nested type of the GRPC service type.
  • The client type always inherits from the C++ SDK Interface Type and manages objects of the ::StubInterface nested type of the GRPC service type.

It feels like the client type should have a well-known typedef for the GRPC service type it is associated with:

class FooClient : public Foo {
public:
    using interface_type = Foo;
    using service_type = FooService;
    ...
private:
    using StubType = service_type::StubInterface;
};

And something similar for the server type:

class FooServer : public ResourceServer, public FooService::Server {
public:
    using service_type = FooService;
    using server_type = service_type::Server;
};

That'd hopefully let you reduce this registration to something like:

Registry::register_resource<GizmoClient, GizmoServer>();

Because you could obtain all the other types by way of the well known typedefs. Another option might be to make our own traits class(es) and bake this knowledge in there.

I don't think you should do it in this review, but I think as a subsequent step it would make a lot of sense to use either conventional typedefs or traits classes to teach the type system about the intended relationship between these several classes and allow compile time navigation around the graph.

Whatever we do there though should ensure that knowledge of client side stuff doesn't force knowledge of server side stuff (though the other direction is probably fine).

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I like the look of this. Is there a reason to even include GizmoClient in the register_resource call? If we're fine with knowledge of server side forcing knowledge of client side then I think we can get away with having

using client_type = FooClient;
using interface_type = Foo;

in the FooServer declaration, and then reducing down to Registry::register_resource<FooServer>(); for registration.

Copy link
Member

@acmorrow acmorrow Feb 7, 2024

Choose a reason for hiding this comment

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

As I mentioned in my other comment, I'm curious about whether register_resource is trying to do too much. Am I correct that both FooClient and FooServer do need to be registered, but the former only for client usage and the latter only in a relevant module? If that's the case then I'd be pretty happy keeping FooServer even unaware of FooClient, and just having both know the associated base type.


// Make sure to explicity register resources with custom APIs.
Registry::register_resource<GizmoClient, GizmoServer, GizmoService>(gizmo_api);
Registry::register_resource<SummationClient, SummationServer, SummationService>(summation_api);
Copy link
Member

Choose a reason for hiding this comment

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

I find it a little odd that in complex/client, the registration must make reference to [Gizmo,Summation]Server despite not really needing anything to do with servers, and that in the module main that the registration must make reference to [Gizmo,Summation]Client, which it will presumably never use.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm yeah I agree. Will revisit and see if we can clean that up some.

Copy link
Member

Choose a reason for hiding this comment

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

It makes me wonder whether client and server registration should be separate activities.


class ResourceServer {
public:
virtual API api() const = 0;
Copy link
Member

Choose a reason for hiding this comment

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

Why do ResourceServer's need this now?

Copy link
Member Author

Choose a reason for hiding this comment

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

My initial thinking was: when initializing the server we store all supported ResourceServer instances, but we want to be able to look them up so we store them by API. This is how we get the API from the ResourceRegistration.

However! I now realize that the key in ResourceRegistration is an API already 🤦 so I think we can get rid of this (and do away with the question of the Robot API) entirely.

Comment on lines 14 to 18
Registry::initialize();
for (const auto& rr : Registry::registered_resources()) {
auto new_manager = std::make_shared<ResourceManager>();
auto server = rr.second->create_resource_server(new_manager, *this);
managed_servers_.emplace(server->api(), std::move(server));
Copy link
Member

Choose a reason for hiding this comment

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

Yeah I'm really feeling the lack of clarity around what the root of initialization and ownership is in the SDK. I don't think we are going to get there in this review.

friend bool operator==(const properties& lhs, const properties& rhs);

// functions shared across all components
static std::shared_ptr<ResourceRegistration> resource_registration();
Copy link
Member

Choose a reason for hiding this comment

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

BTW, I should add that I am happy to see the registration entry points removed from the base classes. It was a little weird that the base classes were responsible for registering the client subclasses and associated servers types.

static std::unordered_map<std::string, std::shared_ptr<ModelRegistration>> registered_models();
/// @brief Provide information on registered resource models.
/// @return A map from name to `ModelRegistration` of all registered resource models.
static const std::unordered_map<std::string, std::shared_ptr<ModelRegistration>>&
Copy link
Member

Choose a reason for hiding this comment

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

I have some concerns about these return types. First, it is returning a reference to internal state. Are there concurrency concerns? Second, shared_ptr is const, but the ModelRegistration it points to is not.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ahh good shout out. I think this overlaps with your concerns on the resource_manager method on ResourceServer. I think some revisiting of why we need to expose these types at all, how they're being used, and how to avoid doing so is warranted. I'll do some digging/refactoring.

virtual ~ResourceRegistration();

/// @brief Add `Reconfigure` functionality to a resource.
std::function<std::shared_ptr<Resource>(std::shared_ptr<Resource>, Name)> create_reconfigurable;
Copy link
Member Author

Choose a reason for hiding this comment

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

This wasn't doing anything and the (theoretical) semantics of it were confusing. So I got rid of it. Per RSDK-6484, we should just use a Reconfigurable base class instead.

@stuqdog stuqdog requested a review from acmorrow February 8, 2024 17:20
Copy link
Member

@benjirewis benjirewis left a comment

Choose a reason for hiding this comment

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

Sorry for the delay on review, but this generally LGTM! I have no big comments to add beyond Drew's.

ResourceClientRegistration() = default;
};

// TODO(RSDK-3030): instead of std::functions, consider making these functions
Copy link
Member

Choose a reason for hiding this comment

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

Should we use a different ticket number if this is still a TODO?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ooh yes, good catch! Updated.

Comment on lines -31 to -32
// TODO: make `register_service` take one of our types as an arg rather than a
// grpc service type, and convert under the hood
Copy link
Member

Choose a reason for hiding this comment

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

I think it's fine to just take in the grpc::Service*.

Copy link
Member

@acmorrow acmorrow left a comment

Choose a reason for hiding this comment

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

I'm approving this, more or less as is. I have a few small comments here and there but I don't think they amount to enough that we should do another round of review. I also don't think this is the end of the resource registration story, nor of the driver initialization story. I still have a lot of questions about who owns what, and it feels like there are maybe more concepts in play than are really needed. I also have some lingering concerns around thread safety and potentially racy visibility into internal state. But I'm very pleased with the reduction in boilerplate in the client and server files, and I think the API for registering clients and servers is dramatically improved.

// of that type from the server.
Registry::register_resource(API::get<Gizmo>(), Gizmo::resource_registration());
Registry::register_resource(API::get<Summation>(), Summation::resource_registration());
Registry::register_resource_client<GizmoClient>();
Copy link
Member

Choose a reason for hiding this comment

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

👍

Comment on lines 14 to 27
#include <viam/api/component/base/v1/base.grpc.pb.h>
#include <viam/api/component/board/v1/board.grpc.pb.h>
#include <viam/api/component/camera/v1/camera.grpc.pb.h>
#include <viam/api/component/encoder/v1/encoder.grpc.pb.h>
#include <viam/api/component/generic/v1/generic.grpc.pb.h>
#include <viam/api/component/motor/v1/motor.grpc.pb.h>
#include <viam/api/component/movementsensor/v1/movementsensor.grpc.pb.h>
#include <viam/api/component/powersensor/v1/powersensor.grpc.pb.h>
#include <viam/api/component/sensor/v1/sensor.grpc.pb.h>
#include <viam/api/component/servo/v1/servo.grpc.pb.h>
#include <viam/api/robot/v1/robot.pb.h>

#include <viam/api/service/generic/v1/generic.grpc.pb.h>
#include <viam/api/service/mlmodel/v1/mlmodel.grpc.pb.h>
#include <viam/api/service/motion/v1/motion.grpc.pb.h>
Copy link
Member

Choose a reason for hiding this comment

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

My intuition is that most of these includes are no longer needed, since I'd expect the associated foo/client.hpp and foo/server.hpp to pull them in. But I haven't checked explicitly.

};

void Registry::register_model(std::shared_ptr<ModelRegistration> resource) {
void Registry::register_model(std::shared_ptr<const ModelRegistration> resource) {
Copy link
Member

Choose a reason for hiding this comment

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

Could you have a look through these methods and the following for value parameters that could be const& (if they are just observed), or are consumed but aren't std::move'd at the point of consumption? I don't want to nitpick each one but I think several of the methods here could use some small improvements.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep will do!

@stuqdog stuqdog merged commit cc2851d into viamrobotics:main Feb 12, 2024
@stuqdog stuqdog deleted the refactor-registry branch February 12, 2024 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants