-
Notifications
You must be signed in to change notification settings - Fork 15
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
Redesign communication client types #409
Redesign communication client types #409
Conversation
robertbartel
commented
Aug 7, 2023
•
edited
Loading
edited
- Separation of transport layer client duties to separate type(s), and moving websocket client to this hierarchy
- Separation of auth to separate types
- Moving to more composition and less strict type hinting in service clients
This is still very much a work in progress, and I haven't even committed all the changes I've been making (just those specifically in |
Redesigning and renaming abstraction, making it more focused on just the transport layer communication handling, rather than general service client behavior; also extending new type with a subtype that adds SSL context security. Making RequestClient concrete but generic. Add ConnectionContextClient as an abstract transport client than maintains a connection via an async managed context, and (re)implementing WebSocketClient using the new (secure) transport client with a context managed connection. Update specialized clients to extend RequestClient Fix ExternalClient with changes.
15271be
to
3ef01cf
Compare
Deprecating to help move away from service-specific types in a general library.
Updating handlers to account for changes to communication package.
The remaining failing tests are in the dmod.client package. That also will have some significant changes coming soon, beyond just what is needed to remedy the failures. Unless there are any objections, I am going to keep those changes separate and give them their own PR. |
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.
In general I think this is a great step in an improved direction! Thanks for reworking this @robertbartel. I left several comments that will require a bit of conversation, but once we've worked through that, we should be on the road. Thanks!
@@ -115,135 +104,277 @@ async def async_recv(self) -> str: | |||
""" | |||
pass | |||
|
|||
|
|||
class SSLSecuredTransportLayerClient(TransportLayerClient, ABC): |
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.
What are your thoughts on removing this and injecting an optional ssl_context
in TransportLayerClient
's initializer?
Something like:
def __init__(self, endpoint_uri: str, *args, ssl_context: Optional[ssl.SSLContext] = None, **kwargs) ): ...
We could then just provide a helper function to create an ssl context from a directory. So pretty much what client_ssl_context
does. Thoughts?
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.
After thinking a little about it, I'd prefer not to accept an SSLContext
directly. I'm splitting hairs a bit, but this strictly speaking would increase the ways we could end up with an improperly/incompletely initialized context in the client. If we did that, I would want us to somehow ensure that the _SSLMethod
was right and that the context had right the CA certs loaded. I think its easier to just be responsible for creating the context object.
We could accept an optional SSL directory as part of the TransportLayerClient
's initializer and create the context there instead of the lazy property. Then we could still remove SSLSecuredTransportLayerClient
. What are your thoughts?
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 that is a fair tradeoff, introducing a little complexity to avoid misuse. My only request would be to provide a way to pass a default ssl context.
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've updated things to optionally bundle the SSL context into TransportLayerClient
, remove SSLSecuredTransportLayerClient
, and parameterize whether to use a default context.
msg = f'********** {self.__class__.__name__} could not deserialize {response_type.__name__} ' \ | ||
f'from raw websocket response: `{str(response_str)}`' | ||
reason = f'{self.__class__.__name__} Could Not Deserialize To {response_type.__name__}' | ||
response_object = response_type(success=False, reason=reason, message=msg, data=response_json) |
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 really wish that we had algebraic datatypes in python, I think that concept would be really useful here. My main concern is returning client side errors as if they are response objects from the server. Meaning, there is no semantic distinction between a server sent failed response and a response the client couldnt handle.
Like you have it written, I dont think we should throw here, but I do think we should either return a tuple of an optional response and an optional client side error or return either a response or a client side error type. Thoughts?
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 not entirely certain we shouldn’t throw here. It is fundamentally an exception to what the client can handle. To properly address it that way, we’d need a (or a few) custom exception type(s) for indicating the specific problem(s). And then we'd need to handle the exception properly somewhere else further up the stack.
I'm not definitively sold on that as a solution, though. My first impression is that it'd be a little less clean, but I'd consider a solution that involved returning a tuple.
Regardless, addressing this properly would be fairly intricate, and what's in place now was carried over from previous code, albeit perhaps arranged slightly differently. I.e., it's out of scope for this PR, but it deserves its own issue.
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.
And then we'd need to handle the exception properly somewhere else further up the stack.
Right, this is my primary motivation for trying to treat the exceptions as values instead of throwing here. Treating them as values without attaching for example, a traceback, could also become difficult to debug. I personally think it is worth the headache though. Exceptions are so easy to ignore / mishandle.
Regardless, addressing this properly would be fairly intricate, and what's in place now was carried over from previous code, albeit perhaps arranged slightly differently. I.e., it's out of scope for this PR, but it deserves its own issue.
Also totally fair, thanks for calling out of the scope creep!
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.
FYI, I've opened #418 for this.
response_json = {} | ||
try: | ||
# Send the request and get the service response | ||
serialized_response = await self._transport_client.async_send(data=str(message), await_response=True) |
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.
Ordering is not guaranteed here so you could get a response message for a different request or different request type all together. At the moment, I dont think that is the case given how we are using websockets currently, but this is certainly something that we will need to prioritize solving soon.
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.
Correct. I'm still considering whether to attempt to address that here or in a subsequent PR, and how to accomplish that.
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.
Thanks for letting me know where you are with this. JLMK if you plan to address it in a separate PR, for now I will treat it like you are going to address it here.
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 am addressing this is a separate PR: #417.
Making this a static method rather than a class method, as it doesn't need any class details.
Moving to top-level interface rather than having subtype for this.
Removing redundant SSLSecuredTransportLayerClient subtype.
Removing redundant SSLSecuredTransportLayerClient from communication package __init__.py.
Modifying TransportLayerClient to expect init params for (most of) the components of a URI, rather than the endpoint URI directly, and adding abstract property so that concrete types can implement this themselves (e.g., to control the specific protocol component used in the URI).
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.
Thanks for working through this, @robertbartel! I left a few comments that mainly address some usability issues. After we work through that, this should be good to go. Thanks!
""" | ||
pass | ||
|
||
def __init__(self, endpoint_host: str, endpoint_port: Union[int, str], endpoint_path: Optional[str] = None, |
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 feels like it is doing a lot, but I think it is alright. Thanks for reworking this @robertbartel! My only comment is, thoughts on making all arguments required keyword arguments? So the signature would change to:
def __init__(
self,
*,
endpoint_host: str,
endpoint_port: Union[int, str],
endpoint_path: Optional[str] = None,
cafile: Optional[Path] = None,
capath: Optional[Path] = None,
use_default_context: bool = False,
**kwargs
):
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.
Sure, I've just made and pushed that change.
""" | ||
pass | ||
|
||
@property | ||
def client_ssl_context(self) -> Optional[ssl.SSLContext]: |
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.
Does this need to be public?
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 don't think so, no. I went ahead and changed that as well.
@@ -117,133 +161,266 @@ async def async_recv(self) -> str: | |||
|
|||
@property | |||
@abstractmethod | |||
def client_ssl_context(self) -> ssl.SSLContext: | |||
def endpoint_uri(self) -> str: |
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.
Does this need to be public? Just trying to reduce the public interface to only what is required.
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'll change this also for now, but it (or something close to it) may have to come back for #417. My reasoning there may have actually been circular, though, so maybe it is superfluous.
@property | ||
def is_new_session(self): | ||
return self._is_new_session | ||
def force_reload(self) -> bool: |
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 looks like it does not need to be reimplemented in the subclass. Am I missing something?
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.
Not sure what you mean. This is being introduced by this class (CachedAuthClient). There is no previous declaration/definition.
return '{}://{}:{}{}'.format(proto, host.strip(), str(port).strip(), path) | ||
|
||
def __init__(self, ssl_directory: Path, *args, **kwargs): | ||
def __init__(self, transport_client: TransportLayerClient, default_response_type: Optional[Type[Response]] = None, |
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 its a good idea to make these keyword only too. Its probably best that we do that with all of these classes given that we have a fair amount of inheritance.
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.
Sure, I've made that change too.
Cleaning up TransportLayerClient __init__ to not pass var/keyword args to super (ABC), and removing abstract annotation from implemented method in WebSocketClient.
Fixing setup of scheduler client unit tests by adding implementation of this classmethod, declared as abstract in upstream superclass.
Removing public property in TransportLayerClient and replacing with use of private attribute.
Clarify that endpoint_host does not include protocol in docstring. Co-authored-by: Austin Raney <austin.raney@noaa.gov>
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.
Thanks for making the changes, @robertbartel! Merge at will!