Skip to content
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

Initial Commit of uP-L2 Communication Layer APIs #119

Merged
merged 2 commits into from
Jun 19, 2024
Merged

Conversation

stevenhartley
Copy link

The following commit includes a proposed common L2 API interfaces that is implemented as static methods that use uTransport (stateless).

#117

Copy link

github-actions bot commented Jun 7, 2024

Code coverage report is ready! 📈

Copy link
Contributor

@tamarafischer tamarafischer left a comment

Choose a reason for hiding this comment

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

I see you have a lot of tests that should give you good coverage.
That said, I am concerned about the value of those tests to really test the scenarios and functionality.

I guess I don't like those register and unregister listeners. They are state on the uTransport that I believe would be a lot more stable if it could be immutable and not have that state changed.

* Notifier is an interface that provides the APIs to send notifications (to a client) or
* register/unregister listeners to receive the notifications.
*/
public interface Notifier {
Copy link
Contributor

Choose a reason for hiding this comment

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

This class does not really do anything. It just wraps the uTransport commands. I don;t see too much value in this kind of wrapper

Copy link
Author

Choose a reason for hiding this comment

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

That is correct but the point is to make it simpler for developers to use a L2 API. These L2 APIs are optional and a developer can re-implement them as they see fit or use L1 APIs directly.

Objects.requireNonNull(transport, UTransport.TRANSPORT_NULL_ERROR);
Objects.requireNonNull(topic, "Notification topic missing");
Objects.requireNonNull(destination, "Notification requires a destination");
return transport.send(UMessageBuilder.notification(topic, destination).build());
Copy link
Contributor

Choose a reason for hiding this comment

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

IMHO it would be better to return the UMessage for notification and then let the caller decide if there is anything to add and then send it to the transport layer.
As an example, if we needed to add a token or another layer of security, then it would need to be added on top of the Message built here and then sent on the transport.
Having the method build the notification message is useful, sending it over the transport is redundant

Copy link
Author

Choose a reason for hiding this comment

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

@tamarafischer, the passed argument is a UMessageBuilder?

Copy link
Author

Choose a reason for hiding this comment

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

@tamarafischer then the developer can call UMessageBuilder to build the notification and then call transport.send() themself, they don't need to use this API.

*/
public class UPayload {

private ByteString data;
Copy link
Contributor

Choose a reason for hiding this comment

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

I was unable to deserialize the ByteString here using an ObjectMapper
I believe a better structure would be a byte[]

Copy link
Author

Choose a reason for hiding this comment

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

Only difference from ByteString and byte[] (per Google documentation) is that ByteString is immutable and byte[] is not. We don't want it to change hence ByteString.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am a fan of immutability, but it can be enforced by the UPayload being a Java 17 record which by default is immutable with an equals, hashcode and toString implemented (they took the idea from scala case classes)

My problem was, when I wanted to serialize the UPayload to a JSON representation, the ByteString did not play nicely with the Jackson ObjectMapper.

Another thing is that if we are already working with a google byte string, why not work with a google Message instead.
Then anyone can turn it into any kind of serialization.

Personally, I am still a fan of the simple byte[]
It gives the best flexibility.

Currently, I will not be able to use UPayload like this in my code and will need to wrap it with something else

Copy link
Author

Choose a reason for hiding this comment

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

Your proposal results then in yet another copy when we build the UMessage that is passed to transport.send().

// automatically send back the response
// NOTE: To indicate a failure, complete the future exceptionally passing a UStatusException
// and the interface will handle propagating the error to the client.
responseFuture.complete(UPayload.packToAny(null));
Copy link
Contributor

Choose a reason for hiding this comment

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

how can this be a happy path test when you are packing null into the response?
If this turns into an object it is still pretty bad and the null is just scary

Copy link
Author

@stevenhartley stevenhartley Jun 11, 2024

Choose a reason for hiding this comment

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

The purpose of the test is to simulate a service that is not sending a response message. I added UPayload.EMPTY so it is clearer

Copy link

Code coverage report is ready! 📈

@stevenhartley stevenhartley force-pushed the comm-layer-apis branch 2 times, most recently from 64fda67 to 62e13c0 Compare June 13, 2024 02:09
Copy link

Code coverage report is ready! 📈

Copy link

Code coverage report is ready! 📈

Copy link

Code coverage report is ready! 📈

* @param timeout The timeout for the method invocation.
* @param priority The priority of the method invocation.
*/
public CallOptions(Integer timeout, UPriority priority) {
Copy link
Contributor

Choose a reason for hiding this comment

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

when working with records it is custom to create static factory methods instead of constructors
just makes it nicer to use
Another thing are the exceptions barfed in the record validation making this difficult to use in a functional way - everything now needs to handle these exceptions.

I guess it is just me not liking code with exceptions and null pointer exceptions.

Copy link
Author

Choose a reason for hiding this comment

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

@tamarafischer is this a must change? what would the static methods look like?

*/
@Override
public UStatus notify(UUri topic, UUri destination, UPayload payload) {
Objects.requireNonNull(transport, UTransport.TRANSPORT_NULL_ERROR);
Copy link
Contributor

Choose a reason for hiding this comment

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

you already tested that you have a transport in the constructor. this might be leftover from when you had the transport as a parameter

Copy link
Author

Choose a reason for hiding this comment

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

Fixed thank you.

*/
public class DefaultNotifier implements Notifier {
// The transport to use for sending the RPC requests
private UTransport transport;
Copy link
Contributor

Choose a reason for hiding this comment

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

so if we wanted a notifier that uses different transports - something you said in a different comment - then I would have to create another instance of the notifier to wrap the transport.

I am not liking the wrappers of uTransport much.
I feel more value is provided by helping create UMessage instances correctly and letting the developer send them to the uTransport of their choice, managing the listeners accordingly.

I feel more value would come from methods that let up build a pipeline of sending messages, reacting upon the data and then performing maybe another task.

An example of a discovery request, when the discovery response comes back, we open the payload and decide what we are going to subscribe to according to the returned calibration information

Copy link
Author

Choose a reason for hiding this comment

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

In that case the developer is free to use the UMessageBuilder and utransport API directly

* @return Returns the {@link UStatus} with the status of the listener registration.
*/
@Override
public UStatus registerNotificationListener(UUri topic, UListener listener) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm, looking at the functions that return UStatus... maybe we could add a map and flatMap to UStatus so we can easily compose functionality.
There is an example in the RpcMapper class

Copy link
Author

Choose a reason for hiding this comment

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

In RpcResult you mean. That would mean I no longer use proto defined uStatus. could we table this as something we can do later? I agree that building the ustatus right now is clunky.

*/
@Override
public UStatus publish(UUri topic, UPayload payload) {
Objects.requireNonNull(topic, "Publish topic missing");
Copy link
Contributor

Choose a reason for hiding this comment

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

same comment as before, you don't need to recheck

* @param listener The listener to be called when a message is received on the topic.
* @return Returns {@link UStatus} with the status of the listener unregister request.
*/
UStatus unregisterListener(UUri topic, UListener listener);
Copy link
Contributor

Choose a reason for hiding this comment

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

unregister listener and I don't even know what the register listener did...
maybe the class cloud help provide the topic that I registered the listener with ...
If the client already needs to keep track for the unregister listener, I see no value in the listener being registered for him.
I would rather have support for a workflow

  1. subscribe
  2. process data from subscription
  3. unsubscribe

that is what my program (a very simple one) would look like

Copy link
Author

Choose a reason for hiding this comment

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

the flow you mentioned works for 2 scenarios:

  1. For temporal subscriptions
  2. For permanent subscriptions when the uE is always running

It doesn't work for the 3rd scenarios where uEs are temporal but we need to be permanently subscribed, (i.e. for events that I want to be woken up for).
How do we support the 3rd use case without the unregisterListener()?

}

@Override
public UStatus unregisterListener(UUri topic, UListener listener) {
Copy link
Contributor

Choose a reason for hiding this comment

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

would make the name unregisterSubscriptionListener like the others



@Override
public CompletionStage<UPayload> invokeMethod(UUri methodUri, UPayload requestPayload, CallOptions options) {
Copy link
Contributor

Choose a reason for hiding this comment

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

more wrappers?!
oh dear

*
* @return the payload data
*/
public ByteString getData() {
Copy link
Contributor

Choose a reason for hiding this comment

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

ah, this is nice

* Close the connection to the transport that will trigger any registered listeners
* to be unregistered.
*/
void close();
Copy link
Contributor

Choose a reason for hiding this comment

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

if the close had a problem, maybe a connection error with the MQTT server or any other network glitch, who's responsibility is it to make sure that everything was indeed cleaned up or maybe things need to be retried?
Is the uTransport responsible to make sure it retried all the cleanup?

Copy link
Author

Choose a reason for hiding this comment

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

The uE that created the uTransport instance is responsible to call close() when it shuts down to clean up.

Copy link

Code coverage report is ready! 📈

Copy link

Code coverage report is ready! 📈

The following commit includes a proposed common L2 API interfaces that is implemented as static methods that use uTransport (stateless).
Copy link

Code coverage report is ready! 📈

Copy link

Code coverage report is ready! 📈

@stevenhartley stevenhartley merged commit f883070 into main Jun 19, 2024
6 checks passed
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.

4 participants