-
Notifications
You must be signed in to change notification settings - Fork 26
RSDK-3030 - Refactor registry #208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…o refactor-registry
| 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. |
There was a problem hiding this comment.
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.
src/viam/sdk/robot/service.hpp
Outdated
|
|
||
| private: | ||
| // CR erodkin: this is a bit of a hacky workaround, leave a comment about it | ||
| API api() const override; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| // TODO: make `register_service` take one of our types as an arg rather than a | ||
| // grpc service type, and convert under the hood |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>&) { |
There was a problem hiding this comment.
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_( |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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.
src/viam/sdk/rpc/server.cpp
Outdated
| 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)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
acmorrow
left a comment
There was a problem hiding this 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.
@acmorrow hi yes, that is a fair assessment! |
|
|
||
| target_sources(viamsdk | ||
| PRIVATE | ||
| # TODO(RSDK-1742): we have to put `registry` up top here because we need the |
There was a problem hiding this comment.
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_) { |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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;
There was a problem hiding this comment.
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.
src/viam/sdk/robot/service.hpp
Outdated
|
|
||
| private: | ||
| // CR erodkin: this is a bit of a hacky workaround, leave a comment about it | ||
| API api() const override; |
There was a problem hiding this comment.
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?
src/viam/sdk/registry/registry.cpp
Outdated
|
|
||
| void register_resources() { | ||
| // Register all components | ||
| Registry::register_resource<BaseClient, BaseServer, component::base::v1::BaseService>( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>()); |
There was a problem hiding this comment.
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
::Servernested type of the GRPC service type. - The client type always inherits from the C++ SDK Interface Type and manages objects of the
::StubInterfacenested 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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
src/viam/sdk/rpc/server.cpp
Outdated
| 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)); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
src/viam/sdk/registry/registry.hpp
Outdated
| 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>>& |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
benjirewis
left a comment
There was a problem hiding this 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.
src/viam/sdk/registry/registry.hpp
Outdated
| ResourceClientRegistration() = default; | ||
| }; | ||
|
|
||
| // TODO(RSDK-3030): instead of std::functions, consider making these functions |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| // TODO: make `register_service` take one of our types as an arg rather than a | ||
| // grpc service type, and convert under the hood |
There was a problem hiding this comment.
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*.
acmorrow
left a comment
There was a problem hiding this 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>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/viam/sdk/registry/registry.cpp
Outdated
| #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> |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep will do!
…into refactor-registry
High Level Changes
initmethod for registering resources pre-main. Instead, call aRegistry::initialize()method to handle all registration.ResourceRegistration2, move it into aregister_resourcemethod.ResourceRegistration2, removing the need to create unique{ResourceType}Registrationclasses.RobotServicesuch that resources acquired from that robot service can actually make gRPC calls.ResourceServersonto theServerclass. I considered creating a top levelViamclass (which would own both the servers and theinitialize()method currently onRegistry), but decided against this because it would then need to be passed around to various methods and thus would complicate user-facing APIs.ModuleService. Previously it was managing services by itself but this ended up being pretty duplicative of the newserver-based ownership module, so I moved it over to just looking to theServerto manage everything.